import { MeasureContext, type WorkspaceIds } from '@hcengineering/core' import { StorageAdapter } from '@hcengineering/server-core' import { createReadStream, createWriteStream, existsSync, statSync } from 'fs' import { mkdir, readdir, readFile, rm, writeFile } from 'fs/promises' import { dirname, join } from 'path' import { PassThrough, Readable, Writable } from 'stream' export interface FileInfo { size: number etag: string contentType?: string lastModified: number } /** * @public */ export interface BackupStorage { loadFile: (name: string) => Promise load: (name: string) => Promise write: (name: string) => Promise writeFile: (name: string, data: string | Buffer | Readable) => Promise exists: (name: string) => Promise stat: (name: string) => Promise statInfo: (name: string) => Promise delete: (name: string) => Promise deleteRecursive: (name: string) => Promise } class FileStorage implements BackupStorage { constructor (readonly root: string) {} async loadFile (name: string): Promise { return await readFile(join(this.root, name)) } async write (name: string): Promise { const fileName = join(this.root, name) const dir = dirname(fileName) if (!existsSync(dir)) { await mkdir(dir, { recursive: true }) } return createWriteStream(join(this.root, name)) } async load (name: string): Promise { return createReadStream(join(this.root, name)) } async exists (name: string): Promise { return existsSync(join(this.root, name)) } async stat (name: string): Promise { return statSync(join(this.root, name)).size } async statInfo (name: string): Promise { const stat = statSync(join(this.root, name)) return { size: stat.size, etag: stat.mtime.toUTCString(), lastModified: stat.mtime.getTime() } } async delete (name: string): Promise { await rm(join(this.root, name)) } async deleteRecursive (name: string): Promise { // Check if folder has no files const files = await readdir(join(this.root, name)) if (files.length === 0) { await rm(join(this.root, name), { recursive: true }) } } async writeFile (name: string, data: string | Buffer | Readable): Promise { const fileName = join(this.root, name) const dir = dirname(fileName) if (!existsSync(dir)) { await mkdir(dir, { recursive: true }) } await writeFile(fileName, data as any) } } class AdapterStorage implements BackupStorage { constructor ( readonly client: StorageAdapter, readonly wsIds: WorkspaceIds, readonly root: string, readonly ctx: MeasureContext ) {} async loadFile (name: string): Promise { const data = await this.client.read(this.ctx, this.wsIds, join(this.root, name)) return Buffer.concat(data as any) } async write (name: string): Promise { const wr = new PassThrough() void this.client.put(this.ctx, this.wsIds, join(this.root, name), wr, 'application/octet-stream') return wr } async load (name: string): Promise { return await this.client.get(this.ctx, this.wsIds, join(this.root, name)) } async exists (name: string): Promise { try { return (await this.client.stat(this.ctx, this.wsIds, join(this.root, name))) !== undefined } catch (err: any) { return false } } async stat (name: string): Promise { try { const st = await this.client.stat(this.ctx, this.wsIds, join(this.root, name)) return st?.size ?? 0 } catch (err: any) { return 0 } } async statInfo (name: string): Promise { try { const st = await this.client.stat(this.ctx, this.wsIds, join(this.root, name)) return { size: st?.size ?? 0, etag: st?.etag ?? '', contentType: st?.contentType, lastModified: st?.modifiedOn ?? 0 } } catch (err: any) { return { size: 0, etag: '', lastModified: 0 } } } async delete (name: string): Promise { await this.client.remove(this.ctx, this.wsIds, [join(this.root, name)]) } async deleteRecursive (name: string): Promise { // Do not need to do anything } async writeFile (name: string, data: string | Buffer | Readable): Promise { // TODO: add mime type detection here. await this.client.put(this.ctx, this.wsIds, join(this.root, name), data, 'application/octet-stream') } } /** * @public */ export async function createFileBackupStorage (fileName: string): Promise { if (!existsSync(fileName)) { console.log(__dirname) await mkdir(fileName, { recursive: true }) } return new FileStorage(fileName) } /** * @public */ export async function createStorageBackupStorage ( ctx: MeasureContext, client: StorageAdapter, wsIds: WorkspaceIds, root: string, check: boolean = true ): Promise { if (check && !(await client.exists(ctx, wsIds))) { await client.make(ctx, wsIds) } return new AdapterStorage(client, wsIds, root, ctx) }