@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.d.cts CHANGED
@@ -1106,6 +1106,8 @@ interface StorageUploadInput {
1106
1106
  size?: number;
1107
1107
  /** SHA-256 of the spooled bytes, computed while they were written. Required with `path`. */
1108
1108
  sha256?: string;
1109
+ /** Message caption that identifies this upload, so a later search can find it. */
1110
+ caption?: string;
1109
1111
  }
1110
1112
  interface StorageHealth {
1111
1113
  /** Reachable and usable. */
@@ -1137,6 +1139,12 @@ interface StorageProvider {
1137
1139
  * the store and stop once `limit` objects are returned.
1138
1140
  */
1139
1141
  listObjects(limit: number): Promise<StoredObject[]>;
1142
+ /**
1143
+ * Finds stored objects whose content hash and size match, read-only. Used to
1144
+ * recover an upload whose outcome was unknown. Providers that cannot search
1145
+ * omit it, and the caller then treats the outcome as unresolved.
1146
+ */
1147
+ findByCaption?(caption: string, size: number): Promise<StoredObject[]>;
1140
1148
  /** Validates that the backing store is reachable and usable. */
1141
1149
  healthCheck(): Promise<StorageHealth>;
1142
1150
  }
@@ -1186,6 +1194,8 @@ declare class StorageEngine {
1186
1194
  exists(ref: StoredRef): Promise<boolean>;
1187
1195
  /** Read-only listing of stored objects, for reconciliation. Never modifies anything. */
1188
1196
  list(limit: number): Promise<StoredObject[]>;
1197
+ /** Read-only search for stored objects with this content hash and size. */
1198
+ findByCaption(caption: string, size: number): Promise<StoredObject[] | null>;
1189
1199
  stat(ref: StoredRef): Promise<StoredObject | null>;
1190
1200
  health(): Promise<StorageHealth>;
1191
1201
  verifyIntegrity(data: Buffer, expectedSha256: string): Promise<boolean>;
@@ -1294,6 +1304,13 @@ declare class FileService {
1294
1304
  * an operation whose object was already stored is committed without uploading.
1295
1305
  */
1296
1306
  private uploadOnce;
1307
+ /**
1308
+ * Resolves an operation whose Telegram outcome is unknown. The channel is searched
1309
+ * for the object by its content hash and size. Exactly one match is adopted, zero
1310
+ * matches means nothing was stored so the upload is sent again, and anything
1311
+ * ambiguous stays UNKNOWN so that no duplicate is written.
1312
+ */
1313
+ private resolveUnknown;
1297
1314
  /**
1298
1315
  * Follows an operation another request is driving, for a bounded time. Returns
1299
1316
  * the finished file if the winner completes, or the stored object's commit if
@@ -1631,6 +1648,11 @@ declare class TelegramStorageProvider implements StorageProvider {
1631
1648
  * service messages are skipped. Nothing is modified.
1632
1649
  */
1633
1650
  listObjects(limit: number): Promise<StoredObject[]>;
1651
+ /**
1652
+ * Finds stored documents whose caption equals the given caption and whose size
1653
+ * matches. Used to recover an upload whose outcome was unknown.
1654
+ */
1655
+ findByCaption(caption: string, size: number): Promise<StoredObject[]>;
1634
1656
  stat(ref: StoredRef): Promise<StoredObject | null>;
1635
1657
  /**
1636
1658
  * Validates that the client is connected and the storage channel is
package/dist/index.d.ts CHANGED
@@ -1106,6 +1106,8 @@ interface StorageUploadInput {
1106
1106
  size?: number;
1107
1107
  /** SHA-256 of the spooled bytes, computed while they were written. Required with `path`. */
1108
1108
  sha256?: string;
1109
+ /** Message caption that identifies this upload, so a later search can find it. */
1110
+ caption?: string;
1109
1111
  }
1110
1112
  interface StorageHealth {
1111
1113
  /** Reachable and usable. */
@@ -1137,6 +1139,12 @@ interface StorageProvider {
1137
1139
  * the store and stop once `limit` objects are returned.
1138
1140
  */
1139
1141
  listObjects(limit: number): Promise<StoredObject[]>;
1142
+ /**
1143
+ * Finds stored objects whose content hash and size match, read-only. Used to
1144
+ * recover an upload whose outcome was unknown. Providers that cannot search
1145
+ * omit it, and the caller then treats the outcome as unresolved.
1146
+ */
1147
+ findByCaption?(caption: string, size: number): Promise<StoredObject[]>;
1140
1148
  /** Validates that the backing store is reachable and usable. */
1141
1149
  healthCheck(): Promise<StorageHealth>;
1142
1150
  }
@@ -1186,6 +1194,8 @@ declare class StorageEngine {
1186
1194
  exists(ref: StoredRef): Promise<boolean>;
1187
1195
  /** Read-only listing of stored objects, for reconciliation. Never modifies anything. */
1188
1196
  list(limit: number): Promise<StoredObject[]>;
1197
+ /** Read-only search for stored objects with this content hash and size. */
1198
+ findByCaption(caption: string, size: number): Promise<StoredObject[] | null>;
1189
1199
  stat(ref: StoredRef): Promise<StoredObject | null>;
1190
1200
  health(): Promise<StorageHealth>;
1191
1201
  verifyIntegrity(data: Buffer, expectedSha256: string): Promise<boolean>;
@@ -1294,6 +1304,13 @@ declare class FileService {
1294
1304
  * an operation whose object was already stored is committed without uploading.
1295
1305
  */
1296
1306
  private uploadOnce;
1307
+ /**
1308
+ * Resolves an operation whose Telegram outcome is unknown. The channel is searched
1309
+ * for the object by its content hash and size. Exactly one match is adopted, zero
1310
+ * matches means nothing was stored so the upload is sent again, and anything
1311
+ * ambiguous stays UNKNOWN so that no duplicate is written.
1312
+ */
1313
+ private resolveUnknown;
1297
1314
  /**
1298
1315
  * Follows an operation another request is driving, for a bounded time. Returns
1299
1316
  * the finished file if the winner completes, or the stored object's commit if
@@ -1631,6 +1648,11 @@ declare class TelegramStorageProvider implements StorageProvider {
1631
1648
  * service messages are skipped. Nothing is modified.
1632
1649
  */
1633
1650
  listObjects(limit: number): Promise<StoredObject[]>;
1651
+ /**
1652
+ * Finds stored documents whose caption equals the given caption and whose size
1653
+ * matches. Used to recover an upload whose outcome was unknown.
1654
+ */
1655
+ findByCaption(caption: string, size: number): Promise<StoredObject[]>;
1634
1656
  stat(ref: StoredRef): Promise<StoredObject | null>;
1635
1657
  /**
1636
1658
  * Validates that the client is connected and the storage channel is
package/dist/index.js CHANGED
@@ -1219,6 +1219,15 @@ var StorageEngine = class {
1219
1219
  throw mapProviderError("Storage listing failed", error);
1220
1220
  }
1221
1221
  }
1222
+ /** Read-only search for stored objects with this content hash and size. */
1223
+ async findByCaption(caption, size) {
1224
+ if (!this.provider.findByCaption) return null;
1225
+ try {
1226
+ return await this.provider.findByCaption(caption, size);
1227
+ } catch (error) {
1228
+ throw mapProviderError("Storage lookup failed", error);
1229
+ }
1230
+ }
1222
1231
  async stat(ref) {
1223
1232
  try {
1224
1233
  return await this.provider.stat(ref);
@@ -1437,6 +1446,15 @@ function uploadRequestFingerprint(input) {
1437
1446
  }
1438
1447
 
1439
1448
  // ../core/src/services/file-service.ts
1449
+ function contentSha256(input) {
1450
+ return input.spooled?.sha256 ?? sha256Hex(input.data ?? Buffer.alloc(0));
1451
+ }
1452
+ function uploadCaption(sha256, operationId) {
1453
+ return `${sha256}:${operationId}`;
1454
+ }
1455
+ function contentSize(input) {
1456
+ return input.spooled?.size ?? input.data?.byteLength ?? 0;
1457
+ }
1440
1458
  function connectionCausedFailure(error) {
1441
1459
  if (error instanceof TelegramConnectionError) return true;
1442
1460
  if (error instanceof UploadFailedError) {
@@ -1494,6 +1512,9 @@ var FileService = class {
1494
1512
  if (op?.status === "UPLOADING" && op.telegramMessageId !== null) {
1495
1513
  return this.commitStoredObject(userId, input, operations, op);
1496
1514
  }
1515
+ if (op?.status === "UNKNOWN") {
1516
+ return this.resolveUnknown(userId, input, operations, op);
1517
+ }
1497
1518
  if (!op) {
1498
1519
  try {
1499
1520
  op = await operations.create({ userId, operationId, requestFingerprint: fingerprint });
@@ -1512,6 +1533,7 @@ var FileService = class {
1512
1533
  }
1513
1534
  try {
1514
1535
  const record = await this.performUpload(userId, input, {
1536
+ caption: uploadCaption(contentSha256(input), op.operationId),
1515
1537
  onStored: async (stored) => {
1516
1538
  await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
1517
1539
  await operations.claim(op.id, "UPLOADING", "COMMITTING");
@@ -1523,11 +1545,54 @@ var FileService = class {
1523
1545
  const status = connectionCausedFailure(error) ? "UNKNOWN" : "FAILED";
1524
1546
  await operations.update(op.id, {
1525
1547
  status,
1548
+ sha256: contentSha256(input),
1549
+ size: contentSize(input),
1526
1550
  error: error instanceof Error ? error.message.slice(0, 200) : "upload failed"
1527
1551
  });
1528
1552
  throw error;
1529
1553
  }
1530
1554
  }
1555
+ /**
1556
+ * Resolves an operation whose Telegram outcome is unknown. The channel is searched
1557
+ * for the object by its content hash and size. Exactly one match is adopted, zero
1558
+ * matches means nothing was stored so the upload is sent again, and anything
1559
+ * ambiguous stays UNKNOWN so that no duplicate is written.
1560
+ */
1561
+ async resolveUnknown(userId, input, operations, op) {
1562
+ if (op.sha256 === null || op.size === null) {
1563
+ throw new ConflictError(
1564
+ "This upload's outcome is unknown and cannot be checked automatically. Ask an administrator to reconcile it."
1565
+ );
1566
+ }
1567
+ const engine = await this.engineFor(userId);
1568
+ const matches = await engine.findByCaption(uploadCaption(op.sha256, op.operationId), op.size);
1569
+ if (matches === null) {
1570
+ throw new ConflictError(
1571
+ "This upload's outcome is unknown and the storage backend cannot search for it."
1572
+ );
1573
+ }
1574
+ if (matches.length > 1) {
1575
+ throw new ConflictError(
1576
+ "This upload's outcome is unknown and more than one matching object exists. Nothing was written."
1577
+ );
1578
+ }
1579
+ if (matches.length === 1) {
1580
+ const stored = matches[0];
1581
+ await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
1582
+ await operations.claim(op.id, "UNKNOWN", "COMMITTING");
1583
+ const record = await this.commitRecord(userId, input, {
1584
+ name: sanitizeFileName(input.name),
1585
+ mimeType: mimeFromFilename(sanitizeFileName(input.name)),
1586
+ size: op.size,
1587
+ sha256: op.sha256,
1588
+ telegramMessageId: Number(stored.messageId)
1589
+ });
1590
+ await operations.update(op.id, { status: "COMPLETED", fileId: record.id });
1591
+ return record;
1592
+ }
1593
+ await operations.claim(op.id, "UNKNOWN", "FAILED");
1594
+ return this.uploadOnce(userId, input, op.operationId, operations);
1595
+ }
1531
1596
  /**
1532
1597
  * Follows an operation another request is driving, for a bounded time. Returns
1533
1598
  * the finished file if the winner completes, or the stored object's commit if
@@ -1605,8 +1670,9 @@ var FileService = class {
1605
1670
  mimeType,
1606
1671
  path: input.spooled.path,
1607
1672
  size: input.spooled.size,
1608
- sha256: input.spooled.sha256
1609
- } : { name, mimeType, data: input.data },
1673
+ sha256: input.spooled.sha256,
1674
+ caption: hooks.caption
1675
+ } : { name, mimeType, data: input.data, caption: hooks.caption },
1610
1676
  control
1611
1677
  );
1612
1678
  await hooks.onStored?.(stored);
@@ -2974,6 +3040,7 @@ var TelegramStorageProvider = class {
2974
3040
  file,
2975
3041
  forceDocument: true,
2976
3042
  workers: 1,
3043
+ caption: input.caption,
2977
3044
  // Surface transport-level progress to the caller. GramJS reports a
2978
3045
  // 0..1 fraction. `isCanceled` lets us abort an in-flight upload.
2979
3046
  progressCallback: Object.assign(
@@ -3131,6 +3198,41 @@ var TelegramStorageProvider = class {
3131
3198
  }
3132
3199
  return objects;
3133
3200
  }
3201
+ /**
3202
+ * Finds stored documents whose caption equals the given caption and whose size
3203
+ * matches. Used to recover an upload whose outcome was unknown.
3204
+ */
3205
+ async findByCaption(caption, size) {
3206
+ const matches = [];
3207
+ try {
3208
+ for await (const message of this.client.iterMessages(this.peer, {
3209
+ search: caption,
3210
+ limit: 20
3211
+ })) {
3212
+ if (!(message instanceof Api2.Message)) continue;
3213
+ if (message.message !== caption) continue;
3214
+ const document = message.document;
3215
+ if (!(document instanceof Api2.Document)) continue;
3216
+ if (Number(document.size.toString()) !== size) continue;
3217
+ let fileName;
3218
+ for (const attr of document.attributes) {
3219
+ if (attr instanceof Api2.DocumentAttributeFilename) {
3220
+ fileName = attr.fileName;
3221
+ break;
3222
+ }
3223
+ }
3224
+ matches.push({
3225
+ messageId: message.id.toString(),
3226
+ name: fileName ?? "file",
3227
+ size,
3228
+ mimeType: document.mimeType
3229
+ });
3230
+ }
3231
+ } catch (error) {
3232
+ throw mapTelegramError(error, "Telegram lookup failed");
3233
+ }
3234
+ return matches;
3235
+ }
3134
3236
  async stat(ref) {
3135
3237
  const message = await this.getMessage(ref.messageId);
3136
3238
  if (!message?.media) return null;