@lacrous/omnicloud 0.2.26 → 0.2.28

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(caption, size) {
1287
+ if (!this.provider.findByCaption) return null;
1288
+ try {
1289
+ return await this.provider.findByCaption(caption, 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,15 @@ 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 uploadCaption(sha256, operationId) {
1516
+ return `${sha256}:${operationId}`;
1517
+ }
1518
+ function contentSize(input) {
1519
+ return input.spooled?.size ?? input.data?.byteLength ?? 0;
1520
+ }
1503
1521
  function connectionCausedFailure(error) {
1504
1522
  if (error instanceof TelegramConnectionError) return true;
1505
1523
  if (error instanceof UploadFailedError) {
@@ -1557,6 +1575,9 @@ var FileService = class {
1557
1575
  if (op?.status === "UPLOADING" && op.telegramMessageId !== null) {
1558
1576
  return this.commitStoredObject(userId, input, operations, op);
1559
1577
  }
1578
+ if (op?.status === "UNKNOWN") {
1579
+ return this.resolveUnknown(userId, input, operations, op);
1580
+ }
1560
1581
  if (!op) {
1561
1582
  try {
1562
1583
  op = await operations.create({ userId, operationId, requestFingerprint: fingerprint });
@@ -1575,6 +1596,7 @@ var FileService = class {
1575
1596
  }
1576
1597
  try {
1577
1598
  const record = await this.performUpload(userId, input, {
1599
+ caption: uploadCaption(contentSha256(input), op.operationId),
1578
1600
  onStored: async (stored) => {
1579
1601
  await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
1580
1602
  await operations.claim(op.id, "UPLOADING", "COMMITTING");
@@ -1586,11 +1608,54 @@ var FileService = class {
1586
1608
  const status = connectionCausedFailure(error) ? "UNKNOWN" : "FAILED";
1587
1609
  await operations.update(op.id, {
1588
1610
  status,
1611
+ sha256: contentSha256(input),
1612
+ size: contentSize(input),
1589
1613
  error: error instanceof Error ? error.message.slice(0, 200) : "upload failed"
1590
1614
  });
1591
1615
  throw error;
1592
1616
  }
1593
1617
  }
1618
+ /**
1619
+ * Resolves an operation whose Telegram outcome is unknown. The channel is searched
1620
+ * for the object by its content hash and size. Exactly one match is adopted, zero
1621
+ * matches means nothing was stored so the upload is sent again, and anything
1622
+ * ambiguous stays UNKNOWN so that no duplicate is written.
1623
+ */
1624
+ async resolveUnknown(userId, input, operations, op) {
1625
+ if (op.sha256 === null || op.size === null) {
1626
+ throw new ConflictError(
1627
+ "This upload's outcome is unknown and cannot be checked automatically. Ask an administrator to reconcile it."
1628
+ );
1629
+ }
1630
+ const engine = await this.engineFor(userId);
1631
+ const matches = await engine.findByCaption(uploadCaption(op.sha256, op.operationId), op.size);
1632
+ if (matches === null) {
1633
+ throw new ConflictError(
1634
+ "This upload's outcome is unknown and the storage backend cannot search for it."
1635
+ );
1636
+ }
1637
+ if (matches.length > 1) {
1638
+ throw new ConflictError(
1639
+ "This upload's outcome is unknown and more than one matching object exists. Nothing was written."
1640
+ );
1641
+ }
1642
+ if (matches.length === 1) {
1643
+ const stored = matches[0];
1644
+ await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
1645
+ await operations.claim(op.id, "UNKNOWN", "COMMITTING");
1646
+ const record = await this.commitRecord(userId, input, {
1647
+ name: sanitizeFileName(input.name),
1648
+ mimeType: mimeFromFilename(sanitizeFileName(input.name)),
1649
+ size: op.size,
1650
+ sha256: op.sha256,
1651
+ telegramMessageId: Number(stored.messageId)
1652
+ });
1653
+ await operations.update(op.id, { status: "COMPLETED", fileId: record.id });
1654
+ return record;
1655
+ }
1656
+ await operations.claim(op.id, "UNKNOWN", "FAILED");
1657
+ return this.uploadOnce(userId, input, op.operationId, operations);
1658
+ }
1594
1659
  /**
1595
1660
  * Follows an operation another request is driving, for a bounded time. Returns
1596
1661
  * the finished file if the winner completes, or the stored object's commit if
@@ -1668,8 +1733,9 @@ var FileService = class {
1668
1733
  mimeType,
1669
1734
  path: input.spooled.path,
1670
1735
  size: input.spooled.size,
1671
- sha256: input.spooled.sha256
1672
- } : { name, mimeType, data: input.data },
1736
+ sha256: input.spooled.sha256,
1737
+ caption: hooks.caption
1738
+ } : { name, mimeType, data: input.data, caption: hooks.caption },
1673
1739
  control
1674
1740
  );
1675
1741
  await hooks.onStored?.(stored);
@@ -3037,6 +3103,7 @@ var TelegramStorageProvider = class {
3037
3103
  file,
3038
3104
  forceDocument: true,
3039
3105
  workers: 1,
3106
+ caption: input.caption,
3040
3107
  // Surface transport-level progress to the caller. GramJS reports a
3041
3108
  // 0..1 fraction. `isCanceled` lets us abort an in-flight upload.
3042
3109
  progressCallback: Object.assign(
@@ -3194,6 +3261,41 @@ var TelegramStorageProvider = class {
3194
3261
  }
3195
3262
  return objects;
3196
3263
  }
3264
+ /**
3265
+ * Finds stored documents whose caption equals the given caption and whose size
3266
+ * matches. Used to recover an upload whose outcome was unknown.
3267
+ */
3268
+ async findByCaption(caption, size) {
3269
+ const matches = [];
3270
+ try {
3271
+ for await (const message of this.client.iterMessages(this.peer, {
3272
+ search: caption,
3273
+ limit: 20
3274
+ })) {
3275
+ if (!(message instanceof import_telegram2.Api.Message)) continue;
3276
+ if (message.message !== caption) continue;
3277
+ const document = message.document;
3278
+ if (!(document instanceof import_telegram2.Api.Document)) continue;
3279
+ if (Number(document.size.toString()) !== size) continue;
3280
+ let fileName;
3281
+ for (const attr of document.attributes) {
3282
+ if (attr instanceof import_telegram2.Api.DocumentAttributeFilename) {
3283
+ fileName = attr.fileName;
3284
+ break;
3285
+ }
3286
+ }
3287
+ matches.push({
3288
+ messageId: message.id.toString(),
3289
+ name: fileName ?? "file",
3290
+ size,
3291
+ mimeType: document.mimeType
3292
+ });
3293
+ }
3294
+ } catch (error) {
3295
+ throw mapTelegramError(error, "Telegram lookup failed");
3296
+ }
3297
+ return matches;
3298
+ }
3197
3299
  async stat(ref) {
3198
3300
  const message = await this.getMessage(ref.messageId);
3199
3301
  if (!message?.media) return null;