(appName: string)
| 317 | * @param appName Application identifier (e.g. 'twitter'), used to build the cloud path prefix |
| 318 | */ |
| 319 | export function createAppFileApi(appName: string): FileOperations { |
| 320 | const basePath = `apps/${appName}/data`; |
| 321 | |
| 322 | /** |
| 323 | * Convert internal path to cloud path |
| 324 | * "/" → "/apps/twitter/data" |
| 325 | * "/posts/tweet.json" → "/apps/twitter/data/posts/tweet.json" |
| 326 | */ |
| 327 | const toCloudPath = (internalPath: string): string => { |
| 328 | const normalized = normalizePath(internalPath); |
| 329 | return normalized === '/' ? `/${basePath}` : `/${basePath}${normalized}`; |
| 330 | }; |
| 331 | |
| 332 | return { |
| 333 | /** |
| 334 | * List files (single level) |
| 335 | * Cloud path is prefixed; returned FileNode.path remains in internal format |
| 336 | */ |
| 337 | listFiles: async (path = '/'): Promise<FileNode[]> => { |
| 338 | const manager = getClientComManager(); |
| 339 | const normalizedPath = normalizePath(path); |
| 340 | |
| 341 | // Internal directory path (without prefix), used to build returned FileNode.path |
| 342 | const internalDirPath = path === '/' ? '' : normalizedPath.replace(/^\//, ''); |
| 343 | // Cloud directory path (with prefix), used for API requests |
| 344 | const cloudDirPath = internalDirPath ? `${basePath}/${internalDirPath}` : basePath; |
| 345 | |
| 346 | const startTime = ts(); |
| 347 | const t0 = performance.now(); |
| 348 | const result = await manager.listFiles<CloudListResponse>({ path: cloudDirPath }); |
| 349 | console.info( |
| 350 | `[FileApi:${appName}][${startTime}] listFiles "${path}" — ${(performance.now() - t0).toFixed(1)}ms`, |
| 351 | ); |
| 352 | |
| 353 | if (!result || result.not_exists || !result.files) { |
| 354 | return []; |
| 355 | } |
| 356 | |
| 357 | return result.files.map((entry) => { |
| 358 | // entry.path may be a full cloud path (e.g. "apps/twitter/data/posts"), |
| 359 | // a session-prefixed path (e.g. "charId/modId/apps/twitter/data/posts"), |
| 360 | // or a relative name (e.g. "posts"). Extract the entry name relative to the current directory. |
| 361 | let entryName = entry.path; |
| 362 | const cloudDirSuffix = `${cloudDirPath}/`; |
| 363 | const suffixIndex = entryName.indexOf(cloudDirSuffix); |
| 364 | if (suffixIndex !== -1) { |
| 365 | entryName = entryName.slice(suffixIndex + cloudDirSuffix.length); |
| 366 | } |
| 367 | |
| 368 | // Build FileNode.path using internal path (without prefix) |
| 369 | const fullPath = normalizePath( |
| 370 | internalDirPath ? `/${internalDirPath}/${entryName}` : `/${entryName}`, |
| 371 | ); |
| 372 | const isFolder = entry.type === 1; |
| 373 | |
| 374 | return { |
| 375 | id: '', |
| 376 | name: entryName, |
no test coverage detected