@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 +104 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +104 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/server/index.js +105 -3
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
|
-
|
|
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;
|