UNPKG

@push.rocks/smartbucket

Version:

A TypeScript library providing a cloud-agnostic interface for managing object storage with functionalities like bucket management, file and directory operations, and advanced features such as metadata handling and file locking.

421 lines (379 loc) 12 kB
// classes.directory.ts import * as plugins from './plugins.js'; import { Bucket } from './classes.bucket.js'; import { File } from './classes.file.js'; import * as helpers from './helpers.js'; export class Directory { public bucketRef: Bucket; public parentDirectoryRef: Directory; public name: string; public tree!: string[]; public files!: string[]; public folders!: string[]; constructor(bucketRefArg: Bucket, parentDirectory: Directory, name: string) { this.bucketRef = bucketRefArg; this.parentDirectoryRef = parentDirectory; this.name = name; } /** * returns an array of parent directories */ public getParentDirectories(): Directory[] { let parentDirectories: Directory[] = []; if (this.parentDirectoryRef) { parentDirectories.push(this.parentDirectoryRef); parentDirectories = parentDirectories.concat(this.parentDirectoryRef.getParentDirectories()); } return parentDirectories; } /** * returns the directory level */ public getDirectoryLevel(): number { return this.getParentDirectories().length; } /** * updates the base path */ public getBasePath(): string { const parentDirectories = this.getParentDirectories(); let basePath = ''; for (const parentDir of parentDirectories) { if (!parentDir.name && !basePath) { basePath = this.name + '/'; continue; } if (parentDir.name && !basePath) { basePath = parentDir.name + '/' + this.name + '/'; continue; } if (parentDir.name && basePath) { basePath = parentDir.name + '/' + basePath; continue; } } return basePath; } /** * gets a file by name */ public async getFile(optionsArg: { path: string; createWithContents?: string | Buffer; getFromTrash?: boolean; }): Promise<File> { const pathDescriptor = { directory: this, path: optionsArg.path, }; const exists = await this.bucketRef.fastExists({ path: await helpers.reducePathDescriptorToPath(pathDescriptor), }); if (!exists && optionsArg.getFromTrash) { const trash = await this.bucketRef.getTrash(); const trashedFile = await trash.getTrashedFileByOriginalName(pathDescriptor); return trashedFile; } if (!exists && !optionsArg.createWithContents) { throw new Error(`File not found at path '${optionsArg.path}'`); } if (!exists && optionsArg.createWithContents) { await File.create({ directory: this, name: optionsArg.path, contents: optionsArg.createWithContents, }); } return new File({ directoryRefArg: this, fileName: optionsArg.path, }); } /** * Check if a file exists in this directory */ public async fileExists(optionsArg: { path: string }): Promise<boolean> { const pathDescriptor = { directory: this, path: optionsArg.path, }; return this.bucketRef.fastExists({ path: await helpers.reducePathDescriptorToPath(pathDescriptor), }); } /** * Check if a subdirectory exists */ public async directoryExists(dirNameArg: string): Promise<boolean> { const directories = await this.listDirectories(); return directories.some(dir => dir.name === dirNameArg); } /** * Collects all ListObjectsV2 pages for a prefix. */ private async listObjectsV2AllPages(prefix: string, delimiter?: string) { const allContents: plugins.s3._Object[] = []; const allCommonPrefixes: plugins.s3.CommonPrefix[] = []; let continuationToken: string | undefined; do { const command = new plugins.s3.ListObjectsV2Command({ Bucket: this.bucketRef.name, Prefix: prefix, Delimiter: delimiter, ContinuationToken: continuationToken, }); const response = await this.bucketRef.smartbucketRef.storageClient.send(command); if (response.Contents) { allContents.push(...response.Contents); } if (response.CommonPrefixes) { allCommonPrefixes.push(...response.CommonPrefixes); } continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; } while (continuationToken); return { contents: allContents, commonPrefixes: allCommonPrefixes }; } /** * lists all files */ public async listFiles(): Promise<File[]> { const { contents } = await this.listObjectsV2AllPages(this.getBasePath(), '/'); const fileArray: File[] = []; contents.forEach((item) => { if (item.Key && !item.Key.endsWith('/')) { const subtractedPath = item.Key.replace(this.getBasePath(), ''); if (!subtractedPath.includes('/')) { fileArray.push( new File({ directoryRefArg: this, fileName: subtractedPath, }) ); } } }); return fileArray; } /** * lists all folders */ public async listDirectories(): Promise<Directory[]> { try { const { commonPrefixes } = await this.listObjectsV2AllPages(this.getBasePath(), '/'); const directoryArray: Directory[] = []; if (commonPrefixes) { commonPrefixes.forEach((item) => { if (item.Prefix) { const subtractedPath = item.Prefix.replace(this.getBasePath(), ''); if (subtractedPath.endsWith('/')) { const dirName = subtractedPath.slice(0, -1); // Ensure the directory name is not empty (which would indicate the base directory itself) if (dirName) { directoryArray.push(new Directory(this.bucketRef, this, dirName)); } } } }); } return directoryArray; } catch (error) { console.error('Error listing directories:', error); throw error; } } /** * gets an array that has all objects with a certain prefix */ public async getTreeArray() { const command = new plugins.s3.ListObjectsV2Command({ Bucket: this.bucketRef.name, Prefix: this.getBasePath(), Delimiter: '/', }); const response = await this.bucketRef.smartbucketRef.storageClient.send(command); return response.Contents; } /** * gets a sub directory by name */ public async getSubDirectoryByName(dirNameArg: string, optionsArg: { /** * in object storage a directory does not exist if it is empty * this option returns a directory even if it is empty */ getEmptyDirectory?: boolean; /** * in object storage a directory does not exist if it is empty * this option creates a directory even if it is empty using a initializer file */ createWithInitializerFile?: boolean; /** * if the path is a file path, it will be treated as a file and the parent directory will be returned */ couldBeFilePath?: boolean; } = {}): Promise<Directory> { const dirNameArray = dirNameArg.split('/').filter(str => str.trim() !== ""); optionsArg = { getEmptyDirectory: false, createWithInitializerFile: false, ...optionsArg, } const getDirectory = async (directoryArg: Directory, dirNameToSearch: string, isFinalDirectory: boolean) => { const directories = await directoryArg.listDirectories(); let returnDirectory = directories.find((directory) => { return directory.name === dirNameToSearch; }); if (returnDirectory) { return returnDirectory; } if (optionsArg.getEmptyDirectory || optionsArg.createWithInitializerFile) { returnDirectory = new Directory(this.bucketRef, directoryArg, dirNameToSearch); } if (isFinalDirectory && optionsArg.createWithInitializerFile) { returnDirectory?.createEmptyFile('00init.txt'); } return returnDirectory || null; }; if (optionsArg.couldBeFilePath) { const baseDirectory = await this.bucketRef.getBaseDirectory(); const existingFile = await baseDirectory.getFile({ path: dirNameArg, }); if (existingFile) { const adjustedPath = dirNameArg.substring(0, dirNameArg.lastIndexOf('/')); return this.getSubDirectoryByName(adjustedPath); } } let wantedDirectory: Directory | null = null; let counter = 0; for (const dirNameToSearch of dirNameArray) { counter++; const directoryToSearchIn = wantedDirectory ? wantedDirectory : this; wantedDirectory = await getDirectory(directoryToSearchIn, dirNameToSearch, counter === dirNameArray.length); } if (!wantedDirectory) { throw new Error(`Directory not found at path '${dirNameArg}'`); } return wantedDirectory; } /** * moves the directory */ public async move() { // TODO throw new Error('Moving a directory is not yet implemented'); } /** * creates an empty file within this directory * @param relativePathArg */ public async createEmptyFile(relativePathArg: string) { const emptyFile = await File.create({ directory: this, name: relativePathArg, contents: '', }); return emptyFile; } // file operations public async fastPut(optionsArg: { path: string; contents: string | Buffer }) { const path = plugins.path.join(this.getBasePath(), optionsArg.path); await this.bucketRef.fastPut({ path, contents: optionsArg.contents, }); } public async fastGet(optionsArg: { path: string }) { const path = plugins.path.join(this.getBasePath(), optionsArg.path); const result = await this.bucketRef.fastGet({ path, }); return result; } public fastGetStream( optionsArg: { path: string; }, typeArg: 'webstream' ): Promise<ReadableStream>; public async fastGetStream( optionsArg: { path: string; }, typeArg: 'nodestream' ): Promise<plugins.stream.Readable>; /** * fastGetStream * @param optionsArg * @returns */ public async fastGetStream( optionsArg: { path: string }, typeArg: 'webstream' | 'nodestream' ): Promise<ReadableStream | plugins.stream.Readable> { const path = plugins.path.join(this.getBasePath(), optionsArg.path); const result = await this.bucketRef.fastGetStream( { path, }, typeArg as any ); return result; } /** * fast put stream */ public async fastPutStream(optionsArg: { path: string; stream: plugins.stream.Readable; }): Promise<void> { const path = plugins.path.join(this.getBasePath(), optionsArg.path); await this.bucketRef.fastPutStream({ path, readableStream: optionsArg.stream, }); } /** * removes a file within the directory * uses file class to make sure effects for metadata etc. are handled correctly * @param optionsArg */ public async fastRemove(optionsArg: { path: string /** * wether the file should be placed into trash. Default is false. */ mode?: 'permanent' | 'trash'; }) { const file = await this.getFile({ path: optionsArg.path, }); await file.delete({ mode: optionsArg.mode ? optionsArg.mode : 'permanent', }); } /** * deletes the directory with all its contents */ public async delete(optionsArg: { mode?: 'permanent' | 'trash'; }) { const deleteDirectory = async (directoryArg: Directory) => { const childDirectories = await directoryArg.listDirectories(); if (childDirectories.length === 0) { console.log('Directory empty! Path complete!'); } else { for (const childDir of childDirectories) { await deleteDirectory(childDir); } } const files = await directoryArg.listFiles(); for (const file of files) { await file.delete({ mode: optionsArg.mode ? optionsArg.mode : 'permanent', }) } }; await deleteDirectory(this); } }