smartbucket/ts/classes.bucket.ts

527 lines
16 KiB
TypeScript
Raw Normal View History

2024-06-17 14:01:35 +00:00
// classes.bucket.ts
2024-05-20 23:22:21 +00:00
import * as plugins from './plugins.js';
import * as helpers from './helpers.js';
import * as interfaces from './interfaces.js';
2024-05-20 23:22:21 +00:00
import { SmartBucket } from './classes.smartbucket.js';
import { Directory } from './classes.directory.js';
import { File } from './classes.file.js';
2024-06-10 14:47:20 +00:00
import { Trash } from './classes.trash.js';
2019-10-14 18:55:07 +00:00
2024-06-10 14:47:20 +00:00
/**
2024-06-17 14:01:35 +00:00
* The bucket class exposes the basic functionality of a bucket.
2024-06-10 14:47:20 +00:00
* The functions of the bucket alone are enough to
2024-06-17 14:01:35 +00:00
* operate in S3 basic fashion on blobs of data.
2024-06-10 14:47:20 +00:00
*/
2019-10-15 12:16:28 +00:00
export class Bucket {
2019-10-15 17:23:06 +00:00
public static async getBucketByName(smartbucketRef: SmartBucket, bucketNameArg: string) {
2024-06-17 14:01:35 +00:00
const command = new plugins.s3.ListBucketsCommand({});
const buckets = await smartbucketRef.s3Client.send(command);
const foundBucket = buckets.Buckets!.find((bucket) => bucket.Name === bucketNameArg);
2019-10-15 12:16:28 +00:00
if (foundBucket) {
2019-10-16 17:15:48 +00:00
console.log(`bucket with name ${bucketNameArg} exists.`);
2019-10-15 12:16:28 +00:00
console.log(`Taking this as base for new Bucket instance`);
return new this(smartbucketRef, bucketNameArg);
2019-10-15 17:23:06 +00:00
} else {
console.log(`did not find bucket by name: ${bucketNameArg}`);
2019-10-15 17:23:06 +00:00
return null;
2019-10-15 12:16:28 +00:00
}
}
2019-10-15 17:23:06 +00:00
public static async createBucketByName(smartbucketRef: SmartBucket, bucketName: string) {
2024-06-17 14:01:35 +00:00
const command = new plugins.s3.CreateBucketCommand({ Bucket: bucketName });
await smartbucketRef.s3Client.send(command).catch((e) => console.log(e));
2019-10-15 17:23:06 +00:00
return new Bucket(smartbucketRef, bucketName);
}
public static async removeBucketByName(smartbucketRef: SmartBucket, bucketName: string) {
2024-06-17 14:01:35 +00:00
const command = new plugins.s3.DeleteBucketCommand({ Bucket: bucketName });
await smartbucketRef.s3Client.send(command).catch((e) => console.log(e));
2019-10-15 17:23:06 +00:00
}
2019-10-15 12:16:28 +00:00
public smartbucketRef: SmartBucket;
public name: string;
2019-10-15 17:23:06 +00:00
2019-10-15 12:16:28 +00:00
constructor(smartbucketRef: SmartBucket, bucketName: string) {
this.smartbucketRef = smartbucketRef;
this.name = bucketName;
}
2019-10-15 17:23:06 +00:00
2019-10-16 17:11:28 +00:00
/**
* gets the base directory of the bucket
*/
public async getBaseDirectory(): Promise<Directory> {
return new Directory(this, null!, '');
2019-10-16 17:11:28 +00:00
}
2024-06-10 14:47:20 +00:00
/**
* gets the trash directory
*/
public async getTrash(): Promise<Trash> {
const trash = new Trash(this);
return trash;
}
public async getDirectoryFromPath(
pathDescriptorArg: interfaces.IPathDecriptor
): Promise<Directory> {
if (!pathDescriptorArg.path && !pathDescriptorArg.directory) {
return this.getBaseDirectory();
}
2024-06-17 14:01:35 +00:00
const checkPath = await helpers.reducePathDescriptorToPath(pathDescriptorArg);
const baseDirectory = await this.getBaseDirectory();
return await baseDirectory.getSubDirectoryByNameStrict(checkPath, {
getEmptyDirectory: true,
});
}
2019-10-16 16:12:18 +00:00
// ===============
// Fast Operations
// ===============
2019-10-15 17:23:06 +00:00
/**
* store file
*/
2024-06-17 14:01:35 +00:00
public async fastPut(
optionsArg: interfaces.IPathDecriptor & {
contents: string | Buffer;
overwrite?: boolean;
}
): Promise<File | null> {
2024-05-20 23:22:21 +00:00
try {
2024-06-10 14:47:20 +00:00
const reducedPath = await helpers.reducePathDescriptorToPath(optionsArg);
const exists = await this.fastExists({ path: reducedPath });
2024-06-10 14:47:20 +00:00
2024-05-20 23:22:21 +00:00
if (exists && !optionsArg.overwrite) {
const errorText = `Object already exists at path '${reducedPath}' in bucket '${this.name}'.`;
console.error(errorText);
return null;
2024-05-20 23:22:21 +00:00
} else if (exists && optionsArg.overwrite) {
2024-06-10 14:47:20 +00:00
console.log(
`Overwriting existing object at path '${reducedPath}' in bucket '${this.name}'.`
);
2024-05-20 23:22:21 +00:00
} else {
console.log(`Creating new object at path '${reducedPath}' in bucket '${this.name}'.`);
2024-05-20 23:22:21 +00:00
}
2024-06-10 14:47:20 +00:00
2024-06-17 14:01:35 +00:00
const command = new plugins.s3.PutObjectCommand({
Bucket: this.name,
Key: reducedPath,
Body: optionsArg.contents,
});
await this.smartbucketRef.s3Client.send(command);
2024-06-10 14:47:20 +00:00
console.log(`Object '${reducedPath}' has been successfully stored in bucket '${this.name}'.`);
const parsedPath = plugins.path.parse(reducedPath);
return new File({
directoryRefArg: await this.getDirectoryFromPath({
path: parsedPath.dir,
}),
fileName: parsedPath.base,
});
2024-05-20 23:22:21 +00:00
} catch (error) {
2024-06-10 14:47:20 +00:00
console.error(
`Error storing object at path '${optionsArg.path}' in bucket '${this.name}':`,
error
);
2024-05-20 23:22:21 +00:00
throw error;
}
2019-10-16 13:21:02 +00:00
}
public async fastPutStrict(...args: Parameters<Bucket['fastPut']>) {
const file = await this.fastPut(...args);
if (!file) {
throw new Error(`File not stored at path '${args[0].path}'`);
}
return file;
}
2019-10-16 13:21:02 +00:00
/**
* get file
*/
2024-06-10 14:47:20 +00:00
public async fastGet(optionsArg: { path: string }): Promise<Buffer> {
2019-10-16 13:21:02 +00:00
const done = plugins.smartpromise.defer();
2020-06-19 00:42:26 +00:00
let completeFile: Buffer;
2024-06-03 19:35:08 +00:00
const replaySubject = await this.fastGetReplaySubject(optionsArg);
2024-05-17 16:53:11 +00:00
const subscription = replaySubject.subscribe({
next: (chunk) => {
2020-06-19 00:42:26 +00:00
if (completeFile) {
completeFile = Buffer.concat([completeFile, chunk]);
2021-04-07 19:01:35 +00:00
} else {
completeFile = chunk;
2020-06-19 00:42:26 +00:00
}
2019-10-20 10:27:58 +00:00
},
2024-05-17 16:53:11 +00:00
complete: () => {
2019-10-20 10:27:58 +00:00
done.resolve();
2021-04-07 18:42:03 +00:00
subscription.unsubscribe();
2024-05-17 16:53:11 +00:00
},
error: (err) => {
console.log(err);
},
});
2019-10-20 10:27:58 +00:00
await done.promise;
return completeFile!;
2019-10-20 10:27:58 +00:00
}
2024-06-03 19:35:08 +00:00
/**
* good when time to first byte is important
* and multiple subscribers are expected
* @param optionsArg
2024-06-10 14:47:20 +00:00
* @returns
2024-06-03 19:35:08 +00:00
*/
public async fastGetReplaySubject(optionsArg: {
2024-05-17 16:53:11 +00:00
path: string;
}): Promise<plugins.smartrx.rxjs.ReplaySubject<Buffer>> {
2024-06-17 14:01:35 +00:00
const command = new plugins.s3.GetObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
2024-05-17 16:53:11 +00:00
});
2024-06-17 14:01:35 +00:00
const response = await this.smartbucketRef.s3Client.send(command);
const replaySubject = new plugins.smartrx.rxjs.ReplaySubject<Buffer>();
2019-10-16 13:21:02 +00:00
2024-06-17 14:01:35 +00:00
// Convert the stream to a format that supports piping
const stream = response.Body as any; // SdkStreamMixin includes readable stream
if (typeof stream.pipe === 'function') {
const duplexStream = new plugins.smartstream.SmartDuplex<Buffer, void>({
writeFunction: async (chunk) => {
replaySubject.next(chunk);
return;
},
finalFunction: async (cb) => {
replaySubject.complete();
return;
},
});
stream.pipe(duplexStream);
2019-10-16 13:21:02 +00:00
}
2019-10-20 10:27:58 +00:00
return replaySubject;
2019-10-16 13:21:02 +00:00
}
2024-06-10 14:47:20 +00:00
public fastGetStream(
optionsArg: {
path: string;
},
typeArg: 'webstream'
): Promise<ReadableStream>;
public async fastGetStream(
optionsArg: {
path: string;
},
typeArg: 'nodestream'
): Promise<plugins.stream.Readable>;
2024-06-03 19:35:08 +00:00
2024-06-10 14:47:20 +00:00
public async fastGetStream(
optionsArg: { path: string },
typeArg: 'webstream' | 'nodestream' = 'nodestream'
): Promise<ReadableStream | plugins.stream.Readable> {
2024-06-17 14:01:35 +00:00
const command = new plugins.s3.GetObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
});
const response = await this.smartbucketRef.s3Client.send(command);
const stream = response.Body as any; // SdkStreamMixin includes readable stream
2024-06-03 19:35:08 +00:00
const duplexStream = new plugins.smartstream.SmartDuplex<Buffer, Buffer>({
writeFunction: async (chunk) => {
return chunk;
},
finalFunction: async (cb) => {
return null!;
2024-06-10 14:47:20 +00:00
},
2024-06-03 19:35:08 +00:00
});
2024-06-17 14:01:35 +00:00
if (typeof stream.pipe === 'function') {
stream.pipe(duplexStream);
2024-06-03 19:35:08 +00:00
}
if (typeArg === 'nodestream') {
return duplexStream;
2024-06-10 14:47:20 +00:00
}
2024-06-03 19:35:08 +00:00
if (typeArg === 'webstream') {
return (await duplexStream.getWebStreams()).readable;
}
throw new Error('unknown typeArg');
2024-06-03 19:35:08 +00:00
}
2024-05-05 17:52:50 +00:00
/**
* store file as stream
*/
public async fastPutStream(optionsArg: {
2024-05-17 16:53:11 +00:00
path: string;
2024-06-09 14:02:33 +00:00
readableStream: plugins.stream.Readable | ReadableStream;
2024-05-17 16:53:11 +00:00
nativeMetadata?: { [key: string]: string };
2024-05-20 23:22:21 +00:00
overwrite?: boolean;
2024-05-05 17:52:50 +00:00
}): Promise<void> {
2024-05-20 23:22:21 +00:00
try {
const exists = await this.fastExists({ path: optionsArg.path });
2024-06-10 14:47:20 +00:00
2024-05-20 23:22:21 +00:00
if (exists && !optionsArg.overwrite) {
2024-06-10 14:47:20 +00:00
console.error(
`Object already exists at path '${optionsArg.path}' in bucket '${this.name}'.`
);
2024-05-20 23:22:21 +00:00
return;
} else if (exists && optionsArg.overwrite) {
2024-06-10 14:47:20 +00:00
console.log(
`Overwriting existing object at path '${optionsArg.path}' in bucket '${this.name}'.`
);
2024-05-20 23:22:21 +00:00
} else {
console.log(`Creating new object at path '${optionsArg.path}' in bucket '${this.name}'.`);
}
2024-06-03 19:35:08 +00:00
2024-06-17 14:01:35 +00:00
const command = new plugins.s3.PutObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
Body: optionsArg.readableStream,
Metadata: optionsArg.nativeMetadata,
});
await this.smartbucketRef.s3Client.send(command);
2024-06-10 14:47:20 +00:00
console.log(
`Object '${optionsArg.path}' has been successfully stored in bucket '${this.name}'.`
2024-05-20 23:22:21 +00:00
);
} catch (error) {
2024-06-10 14:47:20 +00:00
console.error(
`Error storing object at path '${optionsArg.path}' in bucket '${this.name}':`,
error
);
2024-05-20 23:22:21 +00:00
throw error;
}
2024-05-05 17:52:50 +00:00
}
public async fastCopy(optionsArg: {
sourcePath: string;
destinationPath?: string;
2024-05-17 16:53:11 +00:00
targetBucket?: Bucket;
nativeMetadata?: { [key: string]: string };
deleteExistingNativeMetadata?: boolean;
}): Promise<void> {
2024-05-05 17:52:50 +00:00
try {
2024-05-17 16:53:11 +00:00
const targetBucketName = optionsArg.targetBucket ? optionsArg.targetBucket.name : this.name;
2024-05-05 17:52:50 +00:00
// Retrieve current object information to use in copy conditions
2024-06-17 14:01:35 +00:00
const currentObjInfo = await this.smartbucketRef.s3Client.send(
new plugins.s3.HeadObjectCommand({
Bucket: this.name,
Key: optionsArg.sourcePath,
})
2024-05-17 16:53:11 +00:00
);
// Prepare new metadata
const newNativeMetadata = {
2024-06-17 14:01:35 +00:00
...(optionsArg.deleteExistingNativeMetadata ? {} : currentObjInfo.Metadata),
2024-05-17 16:53:11 +00:00
...optionsArg.nativeMetadata,
2024-05-05 17:52:50 +00:00
};
2024-05-17 16:53:11 +00:00
2024-06-17 14:01:35 +00:00
// Define the copy operation
const copySource = `${this.name}/${optionsArg.sourcePath}`;
const command = new plugins.s3.CopyObjectCommand({
Bucket: targetBucketName,
CopySource: copySource,
Key: optionsArg.destinationPath || optionsArg.sourcePath,
Metadata: newNativeMetadata,
MetadataDirective: optionsArg.deleteExistingNativeMetadata ? 'REPLACE' : 'COPY',
});
await this.smartbucketRef.s3Client.send(command);
2024-05-05 17:52:50 +00:00
} catch (err) {
console.error('Error updating metadata:', err);
throw err; // rethrow to allow caller to handle
}
}
2024-06-10 14:47:20 +00:00
/**
* Move object from one path to another within the same bucket or to another bucket
*/
2024-06-10 14:47:20 +00:00
public async fastMove(optionsArg: {
sourcePath: string;
destinationPath: string;
targetBucket?: Bucket;
overwrite?: boolean;
}): Promise<void> {
try {
const destinationBucket = optionsArg.targetBucket || this;
const exists = await destinationBucket.fastExists({
path: optionsArg.destinationPath,
});
2024-06-10 14:47:20 +00:00
if (exists && !optionsArg.overwrite) {
console.error(
`Object already exists at destination path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
);
return;
} else if (exists && optionsArg.overwrite) {
console.log(
`Overwriting existing object at destination path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
);
} else {
console.log(
`Moving object to path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
);
}
2024-06-10 14:47:20 +00:00
await this.fastCopy(optionsArg);
await this.fastRemove({ path: optionsArg.sourcePath });
console.log(
`Object '${optionsArg.sourcePath}' has been successfully moved to '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
);
} catch (error) {
console.error(
`Error moving object from '${optionsArg.sourcePath}' to '${optionsArg.destinationPath}':`,
error
);
throw error;
}
2024-06-10 14:47:20 +00:00
}
2019-10-16 13:21:02 +00:00
/**
* removeObject
*/
2024-06-10 14:47:20 +00:00
public async fastRemove(optionsArg: { path: string }) {
2024-06-17 14:01:35 +00:00
const command = new plugins.s3.DeleteObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
});
await this.smartbucketRef.s3Client.send(command);
2024-05-17 16:53:11 +00:00
}
2024-05-20 23:22:21 +00:00
/**
2024-06-17 14:01:35 +00:00
* check whether file exists
2024-05-20 23:22:21 +00:00
* @param optionsArg
2024-06-10 14:47:20 +00:00
* @returns
2024-05-20 23:22:21 +00:00
*/
2024-06-10 14:47:20 +00:00
public async fastExists(optionsArg: { path: string }): Promise<boolean> {
2024-05-17 16:53:11 +00:00
try {
2024-06-17 14:01:35 +00:00
const command = new plugins.s3.HeadObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
});
await this.smartbucketRef.s3Client.send(command);
2024-05-17 16:53:11 +00:00
console.log(`Object '${optionsArg.path}' exists in bucket '${this.name}'.`);
return true;
} catch (error: any) {
if (error?.name === 'NotFound') {
2024-05-17 16:53:11 +00:00
console.log(`Object '${optionsArg.path}' does not exist in bucket '${this.name}'.`);
return false;
} else {
console.error('Error checking object existence:', error);
throw error; // Rethrow if it's not a NotFound error to handle unexpected issues
}
}
2019-10-15 17:23:06 +00:00
}
2024-05-20 23:22:21 +00:00
2024-06-03 19:35:08 +00:00
/**
* deletes this bucket
*/
public async delete() {
2024-06-17 14:01:35 +00:00
await this.smartbucketRef.s3Client.send(
new plugins.s3.DeleteBucketCommand({ Bucket: this.name })
);
2024-06-03 19:35:08 +00:00
}
public async fastStat(pathDescriptor: interfaces.IPathDecriptor) {
2024-06-17 14:01:35 +00:00
const checkPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
const command = new plugins.s3.HeadObjectCommand({
Bucket: this.name,
Key: checkPath,
});
return this.smartbucketRef.s3Client.send(command);
}
public async isDirectory(pathDescriptor: interfaces.IPathDecriptor): Promise<boolean> {
2024-06-17 14:01:35 +00:00
const checkPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
const command = new plugins.s3.ListObjectsV2Command({
Bucket: this.name,
Prefix: checkPath,
Delimiter: '/',
});
const { CommonPrefixes } = await this.smartbucketRef.s3Client.send(command);
return !!CommonPrefixes && CommonPrefixes.length > 0;
2024-06-10 14:47:20 +00:00
}
public async isFile(pathDescriptor: interfaces.IPathDecriptor): Promise<boolean> {
2024-06-17 14:01:35 +00:00
const checkPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
const command = new plugins.s3.ListObjectsV2Command({
Bucket: this.name,
Prefix: checkPath,
Delimiter: '/',
});
const { Contents } = await this.smartbucketRef.s3Client.send(command);
return !!Contents && Contents.length > 0;
2024-05-20 23:22:21 +00:00
}
public async getMagicBytes(optionsArg: { path: string; length: number }): Promise<Buffer> {
try {
const command = new plugins.s3.GetObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
Range: `bytes=0-${optionsArg.length - 1}`,
});
const response = await this.smartbucketRef.s3Client.send(command);
const chunks = [];
const stream = response.Body as any; // SdkStreamMixin includes readable stream
for await (const chunk of stream) {
chunks.push(chunk);
}
return Buffer.concat(chunks);
} catch (error) {
console.error(
`Error retrieving magic bytes from object at path '${optionsArg.path}' in bucket '${this.name}':`,
error
);
throw error;
}
}
public async cleanAllContents(): Promise<void> {
try {
// Define the command type explicitly
const listCommandInput: plugins.s3.ListObjectsV2CommandInput = {
Bucket: this.name,
};
let isTruncated = true;
let continuationToken: string | undefined = undefined;
while (isTruncated) {
// Add the continuation token to the input if present
const listCommand = new plugins.s3.ListObjectsV2Command({
...listCommandInput,
ContinuationToken: continuationToken,
});
// Explicitly type the response
const response: plugins.s3.ListObjectsV2Output =
await this.smartbucketRef.s3Client.send(listCommand);
console.log(`Cleaning contents of bucket '${this.name}': Now deleting ${response.Contents?.length} items...`);
if (response.Contents && response.Contents.length > 0) {
// Delete objects in batches, mapping each item to { Key: string }
const deleteCommand = new plugins.s3.DeleteObjectsCommand({
Bucket: this.name,
Delete: {
Objects: response.Contents.map((item) => ({ Key: item.Key! })),
Quiet: true,
},
});
await this.smartbucketRef.s3Client.send(deleteCommand);
}
// Update continuation token and truncation status
isTruncated = response.IsTruncated || false;
continuationToken = response.NextContinuationToken;
}
console.log(`All contents in bucket '${this.name}' have been deleted.`);
} catch (error) {
console.error(`Error cleaning contents of bucket '${this.name}':`, error);
throw error;
}
}
2019-10-15 12:16:28 +00:00
}