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.

396 lines (361 loc) 11.1 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 | null> { 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) { return null; } if (!exists && optionsArg.createWithContents) { await File.create({ directory: this, name: optionsArg.path, contents: optionsArg.createWithContents, }); } return new File({ directoryRefArg: this, fileName: optionsArg.path, }); } /** * gets a file strictly * @param args * @returns */ public async getFileStrict(...args: Parameters<Directory['getFile']>) { const file = await this.getFile(...args); if (!file) { throw new Error(`File not found at path '${args[0].path}'`); } return file; } /** * lists all files */ public async listFiles(): Promise<File[]> { const command = new plugins.s3.ListObjectsV2Command({ Bucket: this.bucketRef.name, Prefix: this.getBasePath(), Delimiter: '/', }); const response = await this.bucketRef.smartbucketRef.s3Client.send(command); const fileArray: File[] = []; response.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 command = new plugins.s3.ListObjectsV2Command({ Bucket: this.bucketRef.name, Prefix: this.getBasePath(), Delimiter: '/', }); const response = await this.bucketRef.smartbucketRef.s3Client.send(command); const directoryArray: Directory[] = []; if (response.CommonPrefixes) { response.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.s3Client.send(command); return response.Contents; } /** * gets a sub directory by name */ public async getSubDirectoryByName(dirNameArg: string, optionsArg: { /** * in s3 a directory does not exist if it is empty * this option returns a directory even if it is empty */ getEmptyDirectory?: boolean; /** * in s3 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 | null> { 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, this, 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); } return wantedDirectory || null; } public async getSubDirectoryByNameStrict(...args: Parameters<Directory['getSubDirectoryByName']>) { const directory = await this.getSubDirectoryByName(...args); if (!directory) { throw new Error(`Directory not found at path '${args[0]}'`); } return directory; } /** * 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.getFileStrict({ 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); } }