{"version":3,"file":"fileOperations.mjs","names":["maxMultipartParts: number","maxUploadSize: number","minPartSize: number","preferredPartSize: number","optimalPartSize: number"],"sources":["../../../../src/tus/stores/s3/fileOperations.ts"],"sourcesContent":["import crypto from 'node:crypto'\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { useErrorHandler } from '../../../hooks/useErrorHandler'\nimport { MediaCloudErrors } from '../../../types/errors'\n\nimport type { NodeFSError } from '../../../types'\nimport type AWS from '@aws-sdk/client-s3'\n\ntype CalculateOptimalPartSizeArgs = {\n  size?: number\n}\n\ntype CalculateOffsetFromPartsArgs = {\n  parts?: Array<AWS.Part>\n}\n\ntype CalculatePartNumberArgs = {\n  parts: Array<AWS.Part>\n}\n\ntype GenerateUniqueTmpFileNameArgs = {\n  template: string\n}\n\ntype CalculateOffsetFromPartsExportArgs = {\n  parts?: Array<{ Size?: number }>\n}\n\nconst { throwError } = useErrorHandler()\n\nexport class S3FileOperations {\n  constructor(\n    private maxMultipartParts: number,\n    private maxUploadSize: number,\n    private minPartSize: number,\n    private preferredPartSize: number\n  ) {}\n\n  /**\n   * Calculates the optimal part size for S3 multipart upload\n   * @param args - The function arguments\n   * @param args.size - The upload size in bytes (optional)\n   * @returns The optimal part size in bytes\n   */\n  calculateOptimalPartSize(args: CalculateOptimalPartSizeArgs): number {\n    const { size } = args\n    // When upload size is not known, default to preferredPartSize\n    // Assuming maxUploadSize can lead to extremely large chunks (hundreds of GB),\n    // which breaks streaming/resumable uploads in practice\n    let uploadSize = size\n    if (uploadSize === undefined) {\n      uploadSize = this.preferredPartSize\n    }\n\n    let optimalPartSize: number\n\n    // When upload is smaller or equal to preferredPartSize, upload in just one part\n    if (uploadSize <= this.preferredPartSize) {\n      optimalPartSize = uploadSize\n    }\n    // Does the upload fit in maxMultipartParts parts or less with preferredPartSize\n    else if (uploadSize <= this.preferredPartSize * this.maxMultipartParts) {\n      optimalPartSize = this.preferredPartSize\n      // The upload is too big for the preferred size\n      // We divide the size with the max amount of parts and round it up\n    } else {\n      optimalPartSize = Math.ceil(uploadSize / this.maxMultipartParts)\n    }\n\n    // Always ensure the part size is at least minPartSize\n    return Math.max(optimalPartSize, this.minPartSize)\n  }\n\n  /**\n   * Calculates the offset based on uploaded parts\n   * @param args - The function arguments\n   * @param args.parts - Array of uploaded parts (optional)\n   * @returns The total offset in bytes\n   */\n  calculateOffsetFromParts(args: CalculateOffsetFromPartsArgs): number {\n    const { parts } = args\n    return parts && parts.length > 0\n      ? parts.reduce((a, b) => a + (b.Size ?? 0), 0)\n      : 0\n  }\n\n  /**\n   * Calculates the next part number based on existing parts\n   * @param args - The function arguments\n   * @param args.parts - Array of uploaded parts\n   * @returns The next part number to use\n   */\n  calculatePartNumber(args: CalculatePartNumberArgs): number {\n    const { parts } = args\n    return parts.length > 0 ? parts[parts.length - 1].PartNumber! + 1 : 1\n  }\n\n  /**\n   * Generates a unique temporary file name\n   * @param args - The function arguments\n   * @param args.template - The template string for the file name\n   * @returns Promise that resolves to the unique file path\n   * @throws Error if unable to find unique name after max tries\n   */\n  async generateUniqueTmpFileName(\n    args: GenerateUniqueTmpFileNameArgs\n  ): Promise<string> {\n    const { template } = args\n    const tries = 5\n    for (let i = 0; i < tries; i++) {\n      const randomId = crypto.randomBytes(16).toString('hex')\n      const filePath = path.join(os.tmpdir(), `${template}${randomId}`)\n      try {\n        await fs.promises.access(filePath, fs.constants.F_OK)\n      } catch (error) {\n        const nodeError = error as NodeFSError\n        if (nodeError.code === 'ENOENT') {\n          return filePath\n        }\n      }\n    }\n    throwError(MediaCloudErrors.S3_UNIQUE_NAME_ERROR)\n    throw new Error() // This will never execute but satisfies TypeScript\n  }\n}\n\n/**\n * Calculates the total offset from uploaded parts\n * @param args - The function arguments\n * @param args.parts - Array of parts with size information (optional)\n * @returns The total offset in bytes\n */\nexport function calculateOffsetFromParts(\n  args: CalculateOffsetFromPartsExportArgs\n) {\n  const { parts } = args\n  return parts && parts.length > 0\n    ? parts.reduce((a, b) => a + (b.Size ?? 0), 0)\n    : 0\n}\n"],"mappings":";;;;;;;;AA+BA,MAAM,EAAE,eAAe,iBAAiB;AAExC,IAAa,mBAAb,MAA8B;CAC5B,YACE,AAAQA,mBACR,AAAQC,eACR,AAAQC,aACR,AAAQC,mBACR;EAJQ;EACA;EACA;EACA;;;;;;;;CASV,yBAAyB,MAA4C;EACnE,MAAM,EAAE,SAAS;EAIjB,IAAI,aAAa;AACjB,MAAI,eAAe,OACjB,cAAa,KAAK;EAGpB,IAAIC;AAGJ,MAAI,cAAc,KAAK,kBACrB,mBAAkB;WAGX,cAAc,KAAK,oBAAoB,KAAK,kBACnD,mBAAkB,KAAK;MAIvB,mBAAkB,KAAK,KAAK,aAAa,KAAK,kBAAkB;AAIlE,SAAO,KAAK,IAAI,iBAAiB,KAAK,YAAY;;;;;;;;CASpD,yBAAyB,MAA4C;EACnE,MAAM,EAAE,UAAU;AAClB,SAAO,SAAS,MAAM,SAAS,IAC3B,MAAM,QAAQ,GAAG,MAAM,KAAK,EAAE,QAAQ,IAAI,EAAE,GAC5C;;;;;;;;CASN,oBAAoB,MAAuC;EACzD,MAAM,EAAE,UAAU;AAClB,SAAO,MAAM,SAAS,IAAI,MAAM,MAAM,SAAS,GAAG,aAAc,IAAI;;;;;;;;;CAUtE,MAAM,0BACJ,MACiB;EACjB,MAAM,EAAE,aAAa;EACrB,MAAM,QAAQ;AACd,OAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;GAC9B,MAAM,WAAW,OAAO,YAAY,GAAG,CAAC,SAAS,MAAM;GACvD,MAAM,WAAW,KAAK,KAAK,GAAG,QAAQ,EAAE,GAAG,WAAW,WAAW;AACjE,OAAI;AACF,UAAM,GAAG,SAAS,OAAO,UAAU,GAAG,UAAU,KAAK;YAC9C,OAAO;AAEd,QADkB,MACJ,SAAS,SACrB,QAAO;;;AAIb,aAAW,iBAAiB,qBAAqB;AACjD,QAAM,IAAI,OAAO;;;;;;;;;AAUrB,SAAgB,yBACd,MACA;CACA,MAAM,EAAE,UAAU;AAClB,QAAO,SAAS,MAAM,SAAS,IAC3B,MAAM,QAAQ,GAAG,MAAM,KAAK,EAAE,QAAQ,IAAI,EAAE,GAC5C"}