@lacrous/omnicloud 0.2.25 → 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.d.cts CHANGED
@@ -1137,6 +1137,12 @@ interface StorageProvider {
1137
1137
  * the store and stop once `limit` objects are returned.
1138
1138
  */
1139
1139
  listObjects(limit: number): Promise<StoredObject[]>;
1140
+ /**
1141
+ * Finds stored objects whose content hash and size match, read-only. Used to
1142
+ * recover an upload whose outcome was unknown. Providers that cannot search
1143
+ * omit it, and the caller then treats the outcome as unresolved.
1144
+ */
1145
+ findByCaption?(sha256: string, size: number): Promise<StoredObject[]>;
1140
1146
  /** Validates that the backing store is reachable and usable. */
1141
1147
  healthCheck(): Promise<StorageHealth>;
1142
1148
  }
@@ -1186,6 +1192,8 @@ declare class StorageEngine {
1186
1192
  exists(ref: StoredRef): Promise<boolean>;
1187
1193
  /** Read-only listing of stored objects, for reconciliation. Never modifies anything. */
1188
1194
  list(limit: number): Promise<StoredObject[]>;
1195
+ /** Read-only search for stored objects with this content hash and size. */
1196
+ findByCaption(sha256: string, size: number): Promise<StoredObject[] | null>;
1189
1197
  stat(ref: StoredRef): Promise<StoredObject | null>;
1190
1198
  health(): Promise<StorageHealth>;
1191
1199
  verifyIntegrity(data: Buffer, expectedSha256: string): Promise<boolean>;
@@ -1294,6 +1302,13 @@ declare class FileService {
1294
1302
  * an operation whose object was already stored is committed without uploading.
1295
1303
  */
1296
1304
  private uploadOnce;
1305
+ /**
1306
+ * Resolves an operation whose Telegram outcome is unknown. The channel is searched
1307
+ * for the object by its content hash and size. Exactly one match is adopted, zero
1308
+ * matches means nothing was stored so the upload is sent again, and anything
1309
+ * ambiguous stays UNKNOWN so that no duplicate is written.
1310
+ */
1311
+ private resolveUnknown;
1297
1312
  /**
1298
1313
  * Follows an operation another request is driving, for a bounded time. Returns
1299
1314
  * the finished file if the winner completes, or the stored object's commit if
@@ -1631,6 +1646,11 @@ declare class TelegramStorageProvider implements StorageProvider {
1631
1646
  * service messages are skipped. Nothing is modified.
1632
1647
  */
1633
1648
  listObjects(limit: number): Promise<StoredObject[]>;
1649
+ /**
1650
+ * Finds stored documents whose caption equals the content hash and whose size
1651
+ * matches. Used to recover an upload whose outcome was unknown.
1652
+ */
1653
+ findByCaption(sha256: string, size: number): Promise<StoredObject[]>;
1634
1654
  stat(ref: StoredRef): Promise<StoredObject | null>;
1635
1655
  /**
1636
1656
  * Validates that the client is connected and the storage channel is
package/dist/index.d.ts CHANGED
@@ -1137,6 +1137,12 @@ interface StorageProvider {
1137
1137
  * the store and stop once `limit` objects are returned.
1138
1138
  */
1139
1139
  listObjects(limit: number): Promise<StoredObject[]>;
1140
+ /**
1141
+ * Finds stored objects whose content hash and size match, read-only. Used to
1142
+ * recover an upload whose outcome was unknown. Providers that cannot search
1143
+ * omit it, and the caller then treats the outcome as unresolved.
1144
+ */
1145
+ findByCaption?(sha256: string, size: number): Promise<StoredObject[]>;
1140
1146
  /** Validates that the backing store is reachable and usable. */
1141
1147
  healthCheck(): Promise<StorageHealth>;
1142
1148
  }
@@ -1186,6 +1192,8 @@ declare class StorageEngine {
1186
1192
  exists(ref: StoredRef): Promise<boolean>;
1187
1193
  /** Read-only listing of stored objects, for reconciliation. Never modifies anything. */
1188
1194
  list(limit: number): Promise<StoredObject[]>;
1195
+ /** Read-only search for stored objects with this content hash and size. */
1196
+ findByCaption(sha256: string, size: number): Promise<StoredObject[] | null>;
1189
1197
  stat(ref: StoredRef): Promise<StoredObject | null>;
1190
1198
  health(): Promise<StorageHealth>;
1191
1199
  verifyIntegrity(data: Buffer, expectedSha256: string): Promise<boolean>;
@@ -1294,6 +1302,13 @@ declare class FileService {
1294
1302
  * an operation whose object was already stored is committed without uploading.
1295
1303
  */
1296
1304
  private uploadOnce;
1305
+ /**
1306
+ * Resolves an operation whose Telegram outcome is unknown. The channel is searched
1307
+ * for the object by its content hash and size. Exactly one match is adopted, zero
1308
+ * matches means nothing was stored so the upload is sent again, and anything
1309
+ * ambiguous stays UNKNOWN so that no duplicate is written.
1310
+ */
1311
+ private resolveUnknown;
1297
1312
  /**
1298
1313
  * Follows an operation another request is driving, for a bounded time. Returns
1299
1314
  * the finished file if the winner completes, or the stored object's commit if
@@ -1631,6 +1646,11 @@ declare class TelegramStorageProvider implements StorageProvider {
1631
1646
  * service messages are skipped. Nothing is modified.
1632
1647
  */
1633
1648
  listObjects(limit: number): Promise<StoredObject[]>;
1649
+ /**
1650
+ * Finds stored documents whose caption equals the content hash and whose size
1651
+ * matches. Used to recover an upload whose outcome was unknown.
1652
+ */
1653
+ findByCaption(sha256: string, size: number): Promise<StoredObject[]>;
1634
1654
  stat(ref: StoredRef): Promise<StoredObject | null>;
1635
1655
  /**
1636
1656
  * 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(sha256, size) {
1224
+ if (!this.provider.findByCaption) return null;
1225
+ try {
1226
+ return await this.provider.findByCaption(sha256, 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,12 @@ 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 contentSize(input) {
1453
+ return input.spooled?.size ?? input.data?.byteLength ?? 0;
1454
+ }
1440
1455
  function connectionCausedFailure(error) {
1441
1456
  if (error instanceof TelegramConnectionError) return true;
1442
1457
  if (error instanceof UploadFailedError) {
@@ -1494,6 +1509,9 @@ var FileService = class {
1494
1509
  if (op?.status === "UPLOADING" && op.telegramMessageId !== null) {
1495
1510
  return this.commitStoredObject(userId, input, operations, op);
1496
1511
  }
1512
+ if (op?.status === "UNKNOWN") {
1513
+ return this.resolveUnknown(userId, input, operations, op);
1514
+ }
1497
1515
  if (!op) {
1498
1516
  try {
1499
1517
  op = await operations.create({ userId, operationId, requestFingerprint: fingerprint });
@@ -1523,11 +1541,54 @@ var FileService = class {
1523
1541
  const status = connectionCausedFailure(error) ? "UNKNOWN" : "FAILED";
1524
1542
  await operations.update(op.id, {
1525
1543
  status,
1544
+ sha256: contentSha256(input),
1545
+ size: contentSize(input),
1526
1546
  error: error instanceof Error ? error.message.slice(0, 200) : "upload failed"
1527
1547
  });
1528
1548
  throw error;
1529
1549
  }
1530
1550
  }
1551
+ /**
1552
+ * Resolves an operation whose Telegram outcome is unknown. The channel is searched
1553
+ * for the object by its content hash and size. Exactly one match is adopted, zero
1554
+ * matches means nothing was stored so the upload is sent again, and anything
1555
+ * ambiguous stays UNKNOWN so that no duplicate is written.
1556
+ */
1557
+ async resolveUnknown(userId, input, operations, op) {
1558
+ if (op.sha256 === null || op.size === null) {
1559
+ throw new ConflictError(
1560
+ "This upload's outcome is unknown and cannot be checked automatically. Ask an administrator to reconcile it."
1561
+ );
1562
+ }
1563
+ const engine = await this.engineFor(userId);
1564
+ const matches = await engine.findByCaption(op.sha256, op.size);
1565
+ if (matches === null) {
1566
+ throw new ConflictError(
1567
+ "This upload's outcome is unknown and the storage backend cannot search for it."
1568
+ );
1569
+ }
1570
+ if (matches.length > 1) {
1571
+ throw new ConflictError(
1572
+ "This upload's outcome is unknown and more than one matching object exists. Nothing was written."
1573
+ );
1574
+ }
1575
+ if (matches.length === 1) {
1576
+ const stored = matches[0];
1577
+ await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
1578
+ await operations.claim(op.id, "UNKNOWN", "COMMITTING");
1579
+ const record = await this.commitRecord(userId, input, {
1580
+ name: sanitizeFileName(input.name),
1581
+ mimeType: mimeFromFilename(sanitizeFileName(input.name)),
1582
+ size: op.size,
1583
+ sha256: op.sha256,
1584
+ telegramMessageId: Number(stored.messageId)
1585
+ });
1586
+ await operations.update(op.id, { status: "COMPLETED", fileId: record.id });
1587
+ return record;
1588
+ }
1589
+ await operations.claim(op.id, "UNKNOWN", "FAILED");
1590
+ return this.uploadOnce(userId, input, op.operationId, operations);
1591
+ }
1531
1592
  /**
1532
1593
  * Follows an operation another request is driving, for a bounded time. Returns
1533
1594
  * the finished file if the winner completes, or the stored object's commit if
@@ -2974,6 +3035,7 @@ var TelegramStorageProvider = class {
2974
3035
  file,
2975
3036
  forceDocument: true,
2976
3037
  workers: 1,
3038
+ caption: input.sha256,
2977
3039
  // Surface transport-level progress to the caller. GramJS reports a
2978
3040
  // 0..1 fraction. `isCanceled` lets us abort an in-flight upload.
2979
3041
  progressCallback: Object.assign(
@@ -3131,6 +3193,41 @@ var TelegramStorageProvider = class {
3131
3193
  }
3132
3194
  return objects;
3133
3195
  }
3196
+ /**
3197
+ * Finds stored documents whose caption equals the content hash and whose size
3198
+ * matches. Used to recover an upload whose outcome was unknown.
3199
+ */
3200
+ async findByCaption(sha256, size) {
3201
+ const matches = [];
3202
+ try {
3203
+ for await (const message of this.client.iterMessages(this.peer, {
3204
+ search: sha256,
3205
+ limit: 20
3206
+ })) {
3207
+ if (!(message instanceof Api2.Message)) continue;
3208
+ if (message.message !== sha256) continue;
3209
+ const document = message.document;
3210
+ if (!(document instanceof Api2.Document)) continue;
3211
+ if (Number(document.size.toString()) !== size) continue;
3212
+ let fileName;
3213
+ for (const attr of document.attributes) {
3214
+ if (attr instanceof Api2.DocumentAttributeFilename) {
3215
+ fileName = attr.fileName;
3216
+ break;
3217
+ }
3218
+ }
3219
+ matches.push({
3220
+ messageId: message.id.toString(),
3221
+ name: fileName ?? "file",
3222
+ size,
3223
+ mimeType: document.mimeType
3224
+ });
3225
+ }
3226
+ } catch (error) {
3227
+ throw mapTelegramError(error, "Telegram lookup failed");
3228
+ }
3229
+ return matches;
3230
+ }
3134
3231
  async stat(ref) {
3135
3232
  const message = await this.getMessage(ref.messageId);
3136
3233
  if (!message?.media) return null;