@lacrous/omnicloud 0.2.26 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1282,6 +1282,15 @@ var StorageEngine = class {
1282
1282
  throw mapProviderError("Storage listing failed", error);
1283
1283
  }
1284
1284
  }
1285
+ /** Read-only search for stored objects with this content hash and size. */
1286
+ async findByCaption(sha256, size) {
1287
+ if (!this.provider.findByCaption) return null;
1288
+ try {
1289
+ return await this.provider.findByCaption(sha256, size);
1290
+ } catch (error) {
1291
+ throw mapProviderError("Storage lookup failed", error);
1292
+ }
1293
+ }
1285
1294
  async stat(ref) {
1286
1295
  try {
1287
1296
  return await this.provider.stat(ref);
@@ -1500,6 +1509,12 @@ function uploadRequestFingerprint(input) {
1500
1509
  }
1501
1510
 
1502
1511
  // ../core/src/services/file-service.ts
1512
+ function contentSha256(input) {
1513
+ return input.spooled?.sha256 ?? sha256Hex(input.data ?? Buffer.alloc(0));
1514
+ }
1515
+ function contentSize(input) {
1516
+ return input.spooled?.size ?? input.data?.byteLength ?? 0;
1517
+ }
1503
1518
  function connectionCausedFailure(error) {
1504
1519
  if (error instanceof TelegramConnectionError) return true;
1505
1520
  if (error instanceof UploadFailedError) {
@@ -1557,6 +1572,9 @@ var FileService = class {
1557
1572
  if (op?.status === "UPLOADING" && op.telegramMessageId !== null) {
1558
1573
  return this.commitStoredObject(userId, input, operations, op);
1559
1574
  }
1575
+ if (op?.status === "UNKNOWN") {
1576
+ return this.resolveUnknown(userId, input, operations, op);
1577
+ }
1560
1578
  if (!op) {
1561
1579
  try {
1562
1580
  op = await operations.create({ userId, operationId, requestFingerprint: fingerprint });
@@ -1586,11 +1604,54 @@ var FileService = class {
1586
1604
  const status = connectionCausedFailure(error) ? "UNKNOWN" : "FAILED";
1587
1605
  await operations.update(op.id, {
1588
1606
  status,
1607
+ sha256: contentSha256(input),
1608
+ size: contentSize(input),
1589
1609
  error: error instanceof Error ? error.message.slice(0, 200) : "upload failed"
1590
1610
  });
1591
1611
  throw error;
1592
1612
  }
1593
1613
  }
1614
+ /**
1615
+ * Resolves an operation whose Telegram outcome is unknown. The channel is searched
1616
+ * for the object by its content hash and size. Exactly one match is adopted, zero
1617
+ * matches means nothing was stored so the upload is sent again, and anything
1618
+ * ambiguous stays UNKNOWN so that no duplicate is written.
1619
+ */
1620
+ async resolveUnknown(userId, input, operations, op) {
1621
+ if (op.sha256 === null || op.size === null) {
1622
+ throw new ConflictError(
1623
+ "This upload's outcome is unknown and cannot be checked automatically. Ask an administrator to reconcile it."
1624
+ );
1625
+ }
1626
+ const engine = await this.engineFor(userId);
1627
+ const matches = await engine.findByCaption(op.sha256, op.size);
1628
+ if (matches === null) {
1629
+ throw new ConflictError(
1630
+ "This upload's outcome is unknown and the storage backend cannot search for it."
1631
+ );
1632
+ }
1633
+ if (matches.length > 1) {
1634
+ throw new ConflictError(
1635
+ "This upload's outcome is unknown and more than one matching object exists. Nothing was written."
1636
+ );
1637
+ }
1638
+ if (matches.length === 1) {
1639
+ const stored = matches[0];
1640
+ await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
1641
+ await operations.claim(op.id, "UNKNOWN", "COMMITTING");
1642
+ const record = await this.commitRecord(userId, input, {
1643
+ name: sanitizeFileName(input.name),
1644
+ mimeType: mimeFromFilename(sanitizeFileName(input.name)),
1645
+ size: op.size,
1646
+ sha256: op.sha256,
1647
+ telegramMessageId: Number(stored.messageId)
1648
+ });
1649
+ await operations.update(op.id, { status: "COMPLETED", fileId: record.id });
1650
+ return record;
1651
+ }
1652
+ await operations.claim(op.id, "UNKNOWN", "FAILED");
1653
+ return this.uploadOnce(userId, input, op.operationId, operations);
1654
+ }
1594
1655
  /**
1595
1656
  * Follows an operation another request is driving, for a bounded time. Returns
1596
1657
  * the finished file if the winner completes, or the stored object's commit if
@@ -3037,6 +3098,7 @@ var TelegramStorageProvider = class {
3037
3098
  file,
3038
3099
  forceDocument: true,
3039
3100
  workers: 1,
3101
+ caption: input.sha256,
3040
3102
  // Surface transport-level progress to the caller. GramJS reports a
3041
3103
  // 0..1 fraction. `isCanceled` lets us abort an in-flight upload.
3042
3104
  progressCallback: Object.assign(
@@ -3194,6 +3256,41 @@ var TelegramStorageProvider = class {
3194
3256
  }
3195
3257
  return objects;
3196
3258
  }
3259
+ /**
3260
+ * Finds stored documents whose caption equals the content hash and whose size
3261
+ * matches. Used to recover an upload whose outcome was unknown.
3262
+ */
3263
+ async findByCaption(sha256, size) {
3264
+ const matches = [];
3265
+ try {
3266
+ for await (const message of this.client.iterMessages(this.peer, {
3267
+ search: sha256,
3268
+ limit: 20
3269
+ })) {
3270
+ if (!(message instanceof import_telegram2.Api.Message)) continue;
3271
+ if (message.message !== sha256) continue;
3272
+ const document = message.document;
3273
+ if (!(document instanceof import_telegram2.Api.Document)) continue;
3274
+ if (Number(document.size.toString()) !== size) continue;
3275
+ let fileName;
3276
+ for (const attr of document.attributes) {
3277
+ if (attr instanceof import_telegram2.Api.DocumentAttributeFilename) {
3278
+ fileName = attr.fileName;
3279
+ break;
3280
+ }
3281
+ }
3282
+ matches.push({
3283
+ messageId: message.id.toString(),
3284
+ name: fileName ?? "file",
3285
+ size,
3286
+ mimeType: document.mimeType
3287
+ });
3288
+ }
3289
+ } catch (error) {
3290
+ throw mapTelegramError(error, "Telegram lookup failed");
3291
+ }
3292
+ return matches;
3293
+ }
3197
3294
  async stat(ref) {
3198
3295
  const message = await this.getMessage(ref.messageId);
3199
3296
  if (!message?.media) return null;