@xuda.io/drive_module 1.1.1501 → 1.1.1503
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/index.mjs +101 -6
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -1746,6 +1746,22 @@ export const extract_drive_file = async (req, job_id, headers) => {
|
|
|
1746
1746
|
const ext = path.extname(fileName).toLowerCase();
|
|
1747
1747
|
|
|
1748
1748
|
if (fileName.startsWith('__MACOSX/')) {
|
|
1749
|
+
entry.autodrain();
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// A zip entry name is fully attacker controlled, and this validator used to be
|
|
1754
|
+
// called further down only to decide is_archive, never to reject. That let an entry named
|
|
1755
|
+
// "x.png$(cmd)" through as the doc's file_ext, and file_ext is what ocr_file_to_tmp
|
|
1756
|
+
// appends to the path handed to tesseract, which builds a shell command string.
|
|
1757
|
+
// Directories carry no extension, so only file entries are validated. A rejected
|
|
1758
|
+
// entry has to be drained rather than left unread, or unzipper stalls on the rest
|
|
1759
|
+
// of the archive.
|
|
1760
|
+
const validation_ret = type === 'Directory' ? { valid: false } : file_upload_validator(fileName);
|
|
1761
|
+
|
|
1762
|
+
if (type !== 'Directory' && !validation_ret.valid) {
|
|
1763
|
+
console.warn(`[drive_module] skipped zip entry with unsupported extension: ${path.basename(fileName)}`);
|
|
1764
|
+
entry.autodrain();
|
|
1749
1765
|
return;
|
|
1750
1766
|
}
|
|
1751
1767
|
|
|
@@ -1771,8 +1787,6 @@ export const extract_drive_file = async (req, job_id, headers) => {
|
|
|
1771
1787
|
entry.pipe(fs.createWriteStream(extract_path));
|
|
1772
1788
|
}
|
|
1773
1789
|
|
|
1774
|
-
const validation_ret = file_upload_validator(fileName);
|
|
1775
|
-
|
|
1776
1790
|
let file_doc = {
|
|
1777
1791
|
_id: file_doc_id,
|
|
1778
1792
|
|
|
@@ -1923,6 +1937,69 @@ export const upload_drive_file = async (req, job_id, headers = {}, file_obj) =>
|
|
|
1923
1937
|
};
|
|
1924
1938
|
const check_existing_file_ret = await check_drive_file(check_req);
|
|
1925
1939
|
|
|
1940
|
+
// OVERWRITE IN PLACE. Callers that mean "update this file" (the office plugins editing a
|
|
1941
|
+
// deck / document the user named) opt in with req.overwrite. Without it the block below
|
|
1942
|
+
// dedupes the name, so "update /Presentations/deck.pptx" quietly produced
|
|
1943
|
+
// "deck (1).pptx" and the file the user was looking at never changed.
|
|
1944
|
+
//
|
|
1945
|
+
// The existing doc is REUSED rather than replaced: _id, server_file_name and the bucket
|
|
1946
|
+
// key all stay, so every link already handed out (a chat file card, a shared URL, an
|
|
1947
|
+
// agent's saved reference) still resolves to the edited bytes. Only the mutable facts
|
|
1948
|
+
// move. preview_stat goes back to 1 so the rendered preview is regenerated from the new
|
|
1949
|
+
// content instead of showing the old slides for ever.
|
|
1950
|
+
//
|
|
1951
|
+
// Bucket-backed drives only. The datacenter / is_deployment branches ship bytes over scp
|
|
1952
|
+
// to a path derived from the same name, so an overwrite there would need its own handling;
|
|
1953
|
+
// until it does, those fall through to the dedupe rather than half-writing.
|
|
1954
|
+
if (check_existing_file_ret.code < 0 && req.overwrite) {
|
|
1955
|
+
try {
|
|
1956
|
+
const existing = await get_drive_doc('file', drive_type, app_id, uid, path.join(doc.file_path, originalname), headers);
|
|
1957
|
+
const existing_key = existing?.server_file_name;
|
|
1958
|
+
const app_obj_ret = drive_type === 'workspace' ? await db_module.get_app_obj(app_id) : null;
|
|
1959
|
+
const scp_backed = !!(app_obj_ret?.data?.app_type === 'datacenter' || app_obj_ret?.data?.is_deployment);
|
|
1960
|
+
|
|
1961
|
+
if (existing_key && drive_type !== 'studio' && !scp_backed) {
|
|
1962
|
+
const ref_ow = drive_type === 'user' ? await get_account_default_project_id(uid) : await _common.get_project_app_id(req.app_id, true);
|
|
1963
|
+
existing.bucket = {
|
|
1964
|
+
[`${process.env.XUDA_HOSTNAME}`]: await upload_file_to_spaces(tempPath, ref_ow, drive_type, existing_key),
|
|
1965
|
+
};
|
|
1966
|
+
existing.size = doc.size;
|
|
1967
|
+
existing.mime = doc.mime;
|
|
1968
|
+
existing.file_ext = doc.file_ext;
|
|
1969
|
+
existing.ts = Date.now();
|
|
1970
|
+
// Back to 1 (queued) so the office->pdf render re-runs against the new bytes;
|
|
1971
|
+
// without it the viewer would keep showing the slides from before the edit.
|
|
1972
|
+
// preview_link is not stored, it is derived per read by preview_links_for.
|
|
1973
|
+
existing.preview_stat = doc.preview_stat;
|
|
1974
|
+
|
|
1975
|
+
const save_ow =
|
|
1976
|
+
drive_type === 'user' ? await save_user_drive_file(uid, existing) : await db_module.save_app_couch_doc(await _common.get_project_app_id(req.app_id), existing);
|
|
1977
|
+
if (save_ow.code < 0) throw new Error(save_ow.data);
|
|
1978
|
+
|
|
1979
|
+
await fs_module.unlink(tempPath).catch(() => {});
|
|
1980
|
+
if (existing.preview_stat === 1) {
|
|
1981
|
+
generate_drive_file_preview({ app_id: req.app_id, drive_type, uid, doc_id: existing._id }).catch(() => {});
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
return {
|
|
1985
|
+
code: 760,
|
|
1986
|
+
data: {
|
|
1987
|
+
server_file_name: existing_key,
|
|
1988
|
+
filename: existing.originalname,
|
|
1989
|
+
file_url: get_file_url(drive_type, existing_key, ref_ow),
|
|
1990
|
+
file_path: existing.file_path,
|
|
1991
|
+
bucket: existing.bucket,
|
|
1992
|
+
},
|
|
1993
|
+
overwritten: true,
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
} catch (err) {
|
|
1997
|
+
// An overwrite that cannot be completed must not lose the upload: fall through to the
|
|
1998
|
+
// dedupe path below so the bytes still land somewhere the user can reach.
|
|
1999
|
+
console.error('[upload_drive_file] overwrite failed, falling back to a new name:', err?.message || err);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1926
2003
|
if (check_existing_file_ret.code < 0) {
|
|
1927
2004
|
const rename_file_name = async (i) => {
|
|
1928
2005
|
if (i > 1000) {
|
|
@@ -2427,9 +2504,21 @@ const detectLanguageByCountry = (cfCountry) => {
|
|
|
2427
2504
|
// HTTP entirely. Only a drive with no bucket entry (a datacenter workspace drive, whose
|
|
2428
2505
|
// files are scp'd to the hosting box) still needs the round trip, and that one now uses a
|
|
2429
2506
|
// URL built the same way the drive listing builds it.
|
|
2507
|
+
// The readers below hand this path to a shell: node-tesseract-ocr joins a command string
|
|
2508
|
+
// and calls child_process.exec, and its double quotes around the path still let $(...) and
|
|
2509
|
+
// backticks expand. The upload validator already rejects a filename whose extension is not
|
|
2510
|
+
// on the allowlist, but this is the sink, so it does not get to assume that. Anything that
|
|
2511
|
+
// is not a plain short alphanumeric extension is dropped instead of passed through; the
|
|
2512
|
+
// readers key off the doc's mime, not this suffix, so losing it costs nothing.
|
|
2513
|
+
const SAFE_OCR_EXT = /^\.[a-z0-9]{1,12}$/;
|
|
2514
|
+
const safe_ocr_ext = (doc) => {
|
|
2515
|
+
const candidate = String(doc.file_ext || path.extname(doc.originalname || '') || '').toLowerCase();
|
|
2516
|
+
return SAFE_OCR_EXT.test(candidate) ? candidate : '';
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2430
2519
|
const ocr_file_to_tmp = async (app_id, doc) => {
|
|
2431
2520
|
const write_tmp = async (buffer) => {
|
|
2432
|
-
const tmp_path = path.join(os.tmpdir(), `xuda_ocr_${await _common.xuda_get_uuid('tmp')}${
|
|
2521
|
+
const tmp_path = path.join(os.tmpdir(), `xuda_ocr_${await _common.xuda_get_uuid('tmp')}${safe_ocr_ext(doc)}`);
|
|
2433
2522
|
await fs.promises.writeFile(tmp_path, buffer);
|
|
2434
2523
|
return tmp_path;
|
|
2435
2524
|
};
|
|
@@ -5056,7 +5145,10 @@ export const drive_set_plan = async function (req) {
|
|
|
5056
5145
|
const account = await _drive_account_doc(uid);
|
|
5057
5146
|
if (!account._id) return { code: -404, data: 'account not found' };
|
|
5058
5147
|
const current = _drive_account_plan(account);
|
|
5059
|
-
|
|
5148
|
+
// UI-192: a switch-off asks for the FREE tier, so an account already sitting on free
|
|
5149
|
+
// matches here and would come back unchanged with the switch still on. Asking to
|
|
5150
|
+
// deactivate is never "no change": clearing the chosen stamp is the change.
|
|
5151
|
+
if (current === plan_id && req?.deactivate !== true) return await get_drive_plans({ uid });
|
|
5060
5152
|
|
|
5061
5153
|
// Downgrade guard. Taking space away from an account that is already using it
|
|
5062
5154
|
// would drop it straight into metered overage on files it cannot see a way to
|
|
@@ -5072,7 +5164,7 @@ export const drive_set_plan = async function (req) {
|
|
|
5072
5164
|
|
|
5073
5165
|
const paid = Number(plan.price) > 0;
|
|
5074
5166
|
if (paid) {
|
|
5075
|
-
const ret = await stripe_ms.add_subscription_item({ uid, plan_id });
|
|
5167
|
+
const ret = await stripe_ms.add_subscription_item({ uid, plan_id, deactivate: req?.deactivate === true });
|
|
5076
5168
|
if (!ret || ret.code < 0) return { code: ret?.code === -402 || ret?.code === -3 ? -402 : -1, data: ret?.data || 'could not change the plan' };
|
|
5077
5169
|
} else if (account.stripe_subscription_items?.[DRIVE_CATEGORY]) {
|
|
5078
5170
|
const ret = await stripe_ms.remove_subscription_item({ uid, category: DRIVE_CATEGORY });
|
|
@@ -5082,7 +5174,10 @@ export const drive_set_plan = async function (req) {
|
|
|
5082
5174
|
// Re-read: the stripe call just wrote stripe_subscription_items on this doc.
|
|
5083
5175
|
const fresh = await _drive_account_doc(uid);
|
|
5084
5176
|
fresh.drive_plan = plan_id;
|
|
5085
|
-
|
|
5177
|
+
// UI-192: see the note in shipping_set_plan. A chosen free tier now reads as switched
|
|
5178
|
+
// on, so switching off has to clear the stamp that recorded the choice.
|
|
5179
|
+
if (req?.deactivate === true) delete fresh.drive_plan_changed;
|
|
5180
|
+
else fresh.drive_plan_changed = Date.now();
|
|
5086
5181
|
await db_module.save_couch_doc('xuda_accounts', fresh);
|
|
5087
5182
|
|
|
5088
5183
|
return await get_drive_plans({ uid });
|