{"version":3,"sources":["../../src/FilesSyncNode.ts","../../src/esm-factory.ts"],"sourcesContent":["import { Files } from '@or-sdk/files';\nimport debug from 'debug';\nimport { lookup as mimeLookup } from 'mime-types';\nimport fs from 'node:fs/promises';\nimport { basename, dirname, join, relative } from 'node:path';\n\nimport { esmFactory } from './esm-factory.ts';\nimport type {\n  FilesSyncNodeConfig,\n  FileUploadOptions,\n  LocalObjects,\n  PushLocalPathToFilesOptions,\n  UploadFilesParameters,\n} from './types.ts';\n\nconst log = debug('files-sync');\n\n/**\n * SDK for synchronizing local directory and directory in Onereach Files service.\n * This SDK is designed to be used with Node.js\n *\n * @example\n * Create client instance with service discovery URL\n *\n * ```typescript\n * const client = new FilesSyncNode({\n *   token: process.env.OR_TOKEN,\n *   discoveryUrl: process.env.OR_DISCOVERY_URL\n * });\n * ```\n *\n * @example\n * Create client instance with files service URL\n *\n * ```typescript\n * const client = new FilesSyncNode({\n *   token: process.env.OR_TOKEN,\n *   filesApiUrl: process.env.OR_FILES_API_URL\n * });\n * ```\n *\n * To see debug logs of synching define environment variable `DEBUG` with value `files-sync`:\n *\n * ```sh\n * DEBUG='files-sync' node ./run_logic.js\n * ```\n*/\nexport class FilesSyncNode {\n  private readonly filesClient: Files;\n\n  public constructor({\n    token,\n    discoveryUrl,\n    filesApiUrl,\n    accountId,\n  }: FilesSyncNodeConfig) {\n    const filesConfig = {\n      token,\n      accountId,\n    };\n    this.filesClient = filesApiUrl == undefined\n      ? new Files({\n        ...filesConfig,\n        discoveryUrl,\n      })\n      : new Files({\n        ...filesConfig,\n        filesApiUrl,\n      });\n  }\n\n  /**\n   * Push local directory to Files service.\n   * - All local files would be remote directory;\n   * - Extra files in remote directory will remain;\n   * - If remote file has the same name as local file, it would be replaced.\n   * - Any required remote subdirectories would be created automatically.\n   *\n   * @param localPath local directory path\n   * @param remotePathPrefix path in Files service where to push files. If not set would be synched into root directory.\n   * @param options upload params\n   * @param options.ttl Timestamp in ms when file should expire\n   * @param options.isPublic if `true` uploaded files would be publicly available\n   * (access without authentication). Default `false`.\n   * @param options.concurrencyLimit how many files can be uploaded at the same time. Default `5`.\n   *\n   * @example\n   * Push local directory `./my-data` (path relative to current working directory of the process)\n   * into remote directory `sync-local`\n   * ```typescript\n   * await client.pushLocalPathToFiles('./my-data', 'sync-local')\n   * ```\n   */\n  public async pushLocalPathToFiles(\n    localPath: string,\n    remotePathPrefix?: string,\n    options: PushLocalPathToFilesOptions = {},\n  ) {\n    const localObjects = await this.listLocalObjects(localPath);\n\n    if (localObjects.files.length) {\n      const start = Date.now();\n      log('Found %d local files. Starting upload.', localObjects.files.length);\n      await this.uploadFiles({\n        remotePathPrefix,\n        localFiles: localObjects.files,\n      }, options);\n      log('Finished uploading local files (%d ms)', Date.now() - start);\n    }\n  }\n\n  // private getDiff(localObjects: LocalObjects, remoteObjects: RemoteObjects) {\n  //   const uploadLocalFiles = localObjects.files.filter(localFile => {\n  //     return !remoteObjects.files.some(file => file.relative === localFile.relative);\n  //   });\n\n  //   const deleteRemoteFiles = remoteObjects.files.filter(remoteFile => {\n  //     return !localObjects.files.some(file => file.relative === remoteFile.relative);\n  //   });\n\n  //   const createRemoteDirs = localObjects.dirs.filter(localDir => {\n  //     return !remoteObjects.dirs.some(dir => dir.relative === localDir.relative);\n  //   })\n  //   const deleteRemoteDirs = remoteObjects.dirs.filter(remoteDir => {\n  //     return !localObjects.dirs.some(dir => dir.relative === remoteDir.relative);\n  //   })\n\n  //   return { uploadLocalFiles, deleteRemoteFiles, createRemoteDirs, deleteRemoteDirs };\n  // }\n\n  // private async listRemoteObjects(remotePathPrefix?: string): Promise<RemoteObjects> {\n  //   const result: RemoteObjects = { files: [], dirs: [] }\n  //   const objects = await this.filesClient.getItemsList(remotePathPrefix ?? '');\n\n  //   for (const object of objects) {\n  //     // TODO: test if it works with objects in root without prefix\n  //     const relativePath = relative(object.parentFolder, object.key)\n  //     if (object.key.endsWith('/')) result.dirs.push({ relative: relativePath, key: object.key })\n  //     else result.files.push({ relative: relativePath, key: object.key })\n  //   }\n\n  //   return result;\n  // }\n\n  private async uploadFiles(\n    { localFiles, remotePathPrefix }: UploadFilesParameters,\n    uploadOptions: FileUploadOptions = {},\n  ) {\n    const { concurrencyLimit = 5, isPublic = false, ...options } = uploadOptions;\n\n    const pLimit = await esmFactory.pLimit();\n    const limit = pLimit(concurrencyLimit);\n    const uploadTasks = localFiles.map(localFile => limit(async () => {\n      const start = Date.now();\n      log('Starting upload: %s', localFile.relative);\n\n      const handle = await fs.open(localFile.full, 'r');\n      const stats = await handle.stat();\n      try {\n        const name = basename(localFile.relative);\n        const prefix = join(remotePathPrefix ?? '', dirname(localFile.relative));\n        const request = {\n          ...options,\n          fileName: name,\n          prefix: prefix === '.' ? '' : `${prefix}/`,\n          fileContent: handle.createReadStream({ autoClose: true }),\n          contentType: mimeLookup(name) || 'application/octet-stream',\n          maxFileSize: stats.size,\n          knownLength: stats.size,\n          isPublic,\n        };\n\n        await this.filesClient.uploadFileV2(request);\n        log('Finished upload: %s (%d ms)', localFile.relative, Date.now() - start);\n      } catch (error) {\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        const err: any = error;\n        const errorMessage = err.response?.data ?? err.stack;\n        log('Failed to upload: %s. Error: %j', localFile.relative, errorMessage);\n        throw error;\n      }\n    }));\n\n    const result = await Promise.allSettled(uploadTasks);\n    const rejected = result.find(item => item.status === 'rejected');\n\n    if (rejected) throw rejected.reason;\n  }\n\n  private async listLocalObjects(localPath: string): Promise<LocalObjects> {\n    const { dirs, files } = await this.walkLocal(localPath);\n\n    return {\n      dirs: dirs.map(full => ({\n        full,\n        relative: relative(localPath, full),\n      })),\n      files: files.map(full => ({\n        full,\n        relative: relative(localPath, full),\n      })),\n    };\n  }\n\n  /**\n   * Recursively collect all file paths under a directory.\n   */\n  private async walkLocal(localPath: string) {\n    const entries = await fs.readdir(localPath, { withFileTypes: true });\n    const dirs: string[] = [];\n    const files: string[] = [];\n\n    for (const entry of entries) {\n      const fullPath = join(localPath, entry.name);\n      if (entry.isDirectory()) {\n        dirs.push(join(localPath, entry.name));\n        const child = await this.walkLocal(fullPath);\n        files.push(...child.files);\n        dirs.push(...child.dirs);\n      } else if (entry.isFile()) {\n        files.push(fullPath);\n        // } else {\n        //   files.push(fullPath);\n      }\n    }\n    return {\n      dirs,\n      files,\n    };\n  }\n}\n","import type { LimitFunction } from 'p-limit';\nimport { Memoize } from 'typescript-memoize';\n\nexport class EsmFactory {\n  public async pLimit(): Promise<pLimit> {\n    return await this.importDefault('p-limit');\n  }\n\n  @Memoize()\n  private async importDefault<T>(esmPackage: string): Promise<T> {\n    const pkg = await import(esmPackage);\n    return pkg.default;\n  }\n}\n\nexport const esmFactory = new EsmFactory();\n\nexport type pLimit = (concurrency: number) => LimitFunction;\n"],"mappings":"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","names":["Files","debug","mimeLookup","fs","basename","dirname","join","relative","Memoize","EsmFactory","esmPackage","__decorateClass","Memoize","esmFactory","log","debug","FilesSyncNode","token","discoveryUrl","filesApiUrl","accountId","filesConfig","Files","localPath","remotePathPrefix","options","localObjects","start","localFiles","uploadOptions","concurrencyLimit","isPublic","limit","esmFactory","uploadTasks","localFile","handle","fs","stats","name","basename","prefix","join","dirname","request","mimeLookup","error","err","errorMessage","rejected","item","dirs","files","full","relative","entries","entry","fullPath","child"]}