@xuda.io/drive_module 1.1.1498 → 1.1.1500
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 +274 -0
- package/index_ms.mjs +12 -0
- package/index_msa.mjs +12 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { rimraf } from 'rimraf';
|
|
|
4
4
|
import fse from 'fs-extra';
|
|
5
5
|
import unzipper from 'unzipper';
|
|
6
6
|
import fs from 'fs';
|
|
7
|
+
import os from 'os';
|
|
7
8
|
import tesseract from 'node-tesseract-ocr';
|
|
8
9
|
import countryLanguage from 'country-language';
|
|
9
10
|
import url from 'url';
|
|
@@ -28,6 +29,10 @@ const { get_account_default_project_id } = await import(path.join(process.env.XU
|
|
|
28
29
|
const module_path = path.join(process.env.XUDA_HOME, 'cpi') + (!_conf.is_debug ? '/node_modules/@xuda.io' : '');
|
|
29
30
|
const fs_module = await import(`${module_path}/fs_module/index.mjs`);
|
|
30
31
|
const db_module = await import(`${module_path}/db_module/index.mjs`);
|
|
32
|
+
// UI-113: the Drive storage ladder is one line item on the consolidated
|
|
33
|
+
// subscription, so drive_set_plan drives Stripe the same way every other module
|
|
34
|
+
// tier does (add_subscription_item to buy or reprice, remove on the free tier).
|
|
35
|
+
const stripe_ms = await import(`${module_path}/stripe_module/index_ms.mjs`);
|
|
31
36
|
|
|
32
37
|
// const account_msa = await import(`${module_path}/account_module/index_msa.mjs`);
|
|
33
38
|
|
|
@@ -3763,6 +3768,106 @@ export const read_user_drive_folder = async (req) => {
|
|
|
3763
3768
|
}
|
|
3764
3769
|
};
|
|
3765
3770
|
|
|
3771
|
+
// fetch_drive_file_to_tmp: copy ONE drive file's bytes onto this box as a temp
|
|
3772
|
+
// file so a server-side document generator can open it, edit it and save the
|
|
3773
|
+
// result back as a new drive file. This is what lets the office plugins (pptx,
|
|
3774
|
+
// docx, xlsx) modify a deck/document the user already has instead of only
|
|
3775
|
+
// creating new ones: user + workspace files live in object storage, so there is
|
|
3776
|
+
// otherwise no path on disk to hand to a parser.
|
|
3777
|
+
//
|
|
3778
|
+
// The file is resolved by drive doc id (drv_...) or by its drive path
|
|
3779
|
+
// (/Folder/name.ext). The caller owns the temp file and must unlink it.
|
|
3780
|
+
//
|
|
3781
|
+
// req: { uid, app_id?, drive_type: 'user'|'workspace'|'studio', file }
|
|
3782
|
+
// -> { tmp_path, originalname, file_path, doc_id, size, mime, drive_type }
|
|
3783
|
+
export const fetch_drive_file_to_tmp = async (req) => {
|
|
3784
|
+
try {
|
|
3785
|
+
const { uid, app_id, drive_type = 'user' } = req || {};
|
|
3786
|
+
const file_ref = String(req?.file || req?.file_id || req?.file_path || '').trim();
|
|
3787
|
+
|
|
3788
|
+
if (!uid) return { code: -1, data: 'uid required' };
|
|
3789
|
+
if (!file_ref) return { code: -1, data: 'file required' };
|
|
3790
|
+
validate_drive_type(drive_type);
|
|
3791
|
+
|
|
3792
|
+
const write_tmp = async (buffer, name) => {
|
|
3793
|
+
const tmp_path = path.join(os.tmpdir(), `xuda_drive_${await _common.xuda_get_uuid('tmp')}${path.extname(name) || ''}`);
|
|
3794
|
+
await fs.promises.writeFile(tmp_path, buffer);
|
|
3795
|
+
return tmp_path;
|
|
3796
|
+
};
|
|
3797
|
+
|
|
3798
|
+
// studio drive is plain files on this box, addressed by their relative path.
|
|
3799
|
+
if (drive_type === 'studio') {
|
|
3800
|
+
const ref = await _common.get_project_app_id(app_id, true);
|
|
3801
|
+
const rel = file_ref.replace(/^\/+/, '');
|
|
3802
|
+
const full = path.join(_conf.studio_drive_path, ref, rel);
|
|
3803
|
+
// Contain the read inside the app's own studio folder.
|
|
3804
|
+
const root = path.join(_conf.studio_drive_path, ref);
|
|
3805
|
+
if (!path.resolve(full).startsWith(path.resolve(root) + path.sep)) {
|
|
3806
|
+
return { code: -1, data: 'file_path unauthorized' };
|
|
3807
|
+
}
|
|
3808
|
+
if (!(await fs_module.file_exists(full))) return { code: -1, data: `file not found: ${file_ref}` };
|
|
3809
|
+
const buffer = await fs.promises.readFile(full);
|
|
3810
|
+
const originalname = path.basename(full);
|
|
3811
|
+
return {
|
|
3812
|
+
code: 1,
|
|
3813
|
+
data: {
|
|
3814
|
+
tmp_path: await write_tmp(buffer, originalname),
|
|
3815
|
+
originalname,
|
|
3816
|
+
file_path: path.posix.join('/', path.dirname(rel) === '.' ? '' : path.dirname(rel)),
|
|
3817
|
+
doc_id: rel,
|
|
3818
|
+
size: buffer.length,
|
|
3819
|
+
mime: _drive_content_type_for(originalname),
|
|
3820
|
+
drive_type,
|
|
3821
|
+
},
|
|
3822
|
+
};
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
const project_id = drive_type === 'user' ? await get_account_default_project_id(uid) : await _common.get_project_app_id(app_id);
|
|
3826
|
+
|
|
3827
|
+
// Either the doc id itself, or a /folder/name.ext path within the drive.
|
|
3828
|
+
const by_id = /^drv_/.test(file_ref);
|
|
3829
|
+
const selector = { docType: `${drive_type}_drive`, type: 'file', stat: 3 };
|
|
3830
|
+
if (by_id) {
|
|
3831
|
+
selector._id = file_ref;
|
|
3832
|
+
} else {
|
|
3833
|
+
const with_slash = file_ref.startsWith('/') ? file_ref : `/${file_ref}`;
|
|
3834
|
+
const dir = path.posix.dirname(with_slash);
|
|
3835
|
+
selector.file_path = dir === '.' ? '/' : dir;
|
|
3836
|
+
selector.originalname = path.posix.basename(with_slash);
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
const find_ret = await db_module.find_app_couch_query(project_id, { selector, limit: 1 });
|
|
3840
|
+
const doc = find_ret?.docs?.[0];
|
|
3841
|
+
if (!doc) return { code: -1, data: `file not found: ${file_ref}` };
|
|
3842
|
+
|
|
3843
|
+
const region = _pick_file_region(doc);
|
|
3844
|
+
const b = (region && doc.bucket?.[region]) || {};
|
|
3845
|
+
const Bucket = b.Bucket || b.bucket || (region && _conf.storage_bucket?.[region]?.name);
|
|
3846
|
+
const Key = b.Key || b.key;
|
|
3847
|
+
const s3 = region ? _drive_s3_for(region) : null;
|
|
3848
|
+
if (!s3 || !Bucket || !Key) return { code: -1, data: 'file bytes unavailable' };
|
|
3849
|
+
|
|
3850
|
+
const obj = await s3.send(new GetObjectCommand({ Bucket, Key }));
|
|
3851
|
+
const buffer = await _stream_to_buffer(obj.Body);
|
|
3852
|
+
const originalname = doc.originalname || path.basename(Key);
|
|
3853
|
+
|
|
3854
|
+
return {
|
|
3855
|
+
code: 1,
|
|
3856
|
+
data: {
|
|
3857
|
+
tmp_path: await write_tmp(buffer, originalname),
|
|
3858
|
+
originalname,
|
|
3859
|
+
file_path: doc.file_path || '/',
|
|
3860
|
+
doc_id: doc._id,
|
|
3861
|
+
size: buffer.length,
|
|
3862
|
+
mime: doc.mime || _drive_content_type_for(originalname),
|
|
3863
|
+
drive_type,
|
|
3864
|
+
},
|
|
3865
|
+
};
|
|
3866
|
+
} catch (err) {
|
|
3867
|
+
return { code: -400, data: err.message };
|
|
3868
|
+
}
|
|
3869
|
+
};
|
|
3870
|
+
|
|
3766
3871
|
// put_site_files — snapshot a set of static-site files into ONE region's
|
|
3767
3872
|
// Spaces bucket under sites/{site_id}/{version}/{path}, public-read. Each
|
|
3768
3873
|
// deploy writes a fresh immutable version directory, so older versions stay
|
|
@@ -4369,3 +4474,172 @@ export const file_upload_validator = function (file_name) {
|
|
|
4369
4474
|
return { valid: false, error: `Unsupported file extension: .${ext}` };
|
|
4370
4475
|
}
|
|
4371
4476
|
};
|
|
4477
|
+
|
|
4478
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4479
|
+
// DRIVE PLANS (UI-113)
|
|
4480
|
+
//
|
|
4481
|
+
// Drive is sold on its own drive_* ladder and what the ladder sells is SPACE.
|
|
4482
|
+
// A tier's `flags.extra_gb` is added on top of the storage the account already
|
|
4483
|
+
// has from its membership plan and its AI workspace plan, it does not replace
|
|
4484
|
+
// either, so an account that never picks a tier is on drive_free and nothing
|
|
4485
|
+
// about its Drive changes.
|
|
4486
|
+
//
|
|
4487
|
+
// The money is one line item on the consolidated subscription, the same shape
|
|
4488
|
+
// every other module tier uses: a paid tier creates or reprices the line, free
|
|
4489
|
+
// removes it. Billing moves FIRST and the plan is only written once Stripe
|
|
4490
|
+
// agreed, so a failed charge can never leave an account holding space it is
|
|
4491
|
+
// not paying for.
|
|
4492
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4493
|
+
|
|
4494
|
+
const DRIVE_CATEGORY = 'drive';
|
|
4495
|
+
const GB = 1073741824;
|
|
4496
|
+
|
|
4497
|
+
const _drive_tiers = () =>
|
|
4498
|
+
Object.values(_conf.PLAN_OBJ || {})
|
|
4499
|
+
.filter((p) => p.category === DRIVE_CATEGORY)
|
|
4500
|
+
.sort((a, b) => (Number(a.rank) || 0) - (Number(b.rank) || 0));
|
|
4501
|
+
|
|
4502
|
+
const _drive_plan = (plan_id) => {
|
|
4503
|
+
const plan = _conf.PLAN_OBJ?.[plan_id];
|
|
4504
|
+
return plan && plan.category === DRIVE_CATEGORY ? plan : null;
|
|
4505
|
+
};
|
|
4506
|
+
|
|
4507
|
+
const _drive_account_doc = async (uid) => {
|
|
4508
|
+
const ret = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
4509
|
+
return ret.code < 0 || !ret.data ? {} : ret.data;
|
|
4510
|
+
};
|
|
4511
|
+
|
|
4512
|
+
// An account that never touched the ladder is on free. Same default the billing
|
|
4513
|
+
// Modules card applies, kept identical on purpose so the two never disagree.
|
|
4514
|
+
const _drive_account_plan = (account) => (_drive_plan(account?.drive_plan) ? account.drive_plan : 'drive_free');
|
|
4515
|
+
|
|
4516
|
+
// What the account is storing, using the SAME sum the overage metering charges
|
|
4517
|
+
// on (account_module increment_account_usage). project_data_size is deliberately
|
|
4518
|
+
// out of it there, so it is out of it here: a guard that counted bytes the
|
|
4519
|
+
// metering ignores would refuse a downgrade the customer is not billed for.
|
|
4520
|
+
const _drive_used_bytes = (account) =>
|
|
4521
|
+
(account?.user_drive_size || 0) +
|
|
4522
|
+
(account?.studio_drive_size || 0) +
|
|
4523
|
+
(account?.workspace_drive_size || 0) +
|
|
4524
|
+
(account?.builds_drive_size || 0) +
|
|
4525
|
+
(account?.plugins_drive_size || 0);
|
|
4526
|
+
|
|
4527
|
+
// The quota the account WOULD have on a given tier, in bytes. Delegates to
|
|
4528
|
+
// account_module's get_effective_entitlements rather than re-deriving the stack,
|
|
4529
|
+
// because that function is what the metering and every storage bar already use.
|
|
4530
|
+
// Imported lazily: this is the only place in the drive module that needs it and
|
|
4531
|
+
// account_module pulls in a large tree of its own.
|
|
4532
|
+
const _drive_quota_bytes = async (account, plan_id) => {
|
|
4533
|
+
const { get_effective_entitlements } = await import(`${module_path}/account_module/index.mjs`);
|
|
4534
|
+
const ent = get_effective_entitlements({ ...account, drive_plan: plan_id ?? account?.drive_plan });
|
|
4535
|
+
return {
|
|
4536
|
+
unlimited: !Number.isFinite(ent.drive_gb),
|
|
4537
|
+
bytes: Number.isFinite(ent.drive_gb) ? Math.round(ent.drive_gb * GB) : 0,
|
|
4538
|
+
breakdown: ent.drive_breakdown || {},
|
|
4539
|
+
};
|
|
4540
|
+
};
|
|
4541
|
+
|
|
4542
|
+
const _drive_pretty = (bytes) => {
|
|
4543
|
+
if (bytes >= 1099511627776) return `${Math.round((bytes / 1099511627776) * 10) / 10} TB`;
|
|
4544
|
+
if (bytes >= GB) return `${Math.round((bytes / GB) * 10) / 10} GB`;
|
|
4545
|
+
return `${Math.round(bytes / 1048576)} MB`;
|
|
4546
|
+
};
|
|
4547
|
+
|
|
4548
|
+
export const get_drive_plans = async function (req) {
|
|
4549
|
+
try {
|
|
4550
|
+
const uid = req?.uid;
|
|
4551
|
+
if (!uid) return { code: -401, data: 'not authenticated' };
|
|
4552
|
+
const account = await _drive_account_doc(uid);
|
|
4553
|
+
if (!account._id) return { code: -404, data: 'account not found' };
|
|
4554
|
+
|
|
4555
|
+
const plan_id = _drive_account_plan(account);
|
|
4556
|
+
const used = _drive_used_bytes(account);
|
|
4557
|
+
const quota = await _drive_quota_bytes(account, plan_id);
|
|
4558
|
+
|
|
4559
|
+
// Every tier carries the quota it WOULD give and whether the account already
|
|
4560
|
+
// stores more than that, so the screen can mark a tier as out of reach
|
|
4561
|
+
// without doing the entitlement arithmetic itself.
|
|
4562
|
+
const plans = [];
|
|
4563
|
+
for (const p of _drive_tiers()) {
|
|
4564
|
+
const q = await _drive_quota_bytes(account, p.id);
|
|
4565
|
+
plans.push({
|
|
4566
|
+
...p,
|
|
4567
|
+
quota_bytes: q.bytes,
|
|
4568
|
+
quota_unlimited: q.unlimited,
|
|
4569
|
+
fits: q.unlimited || used <= q.bytes,
|
|
4570
|
+
});
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4573
|
+
return {
|
|
4574
|
+
code: 1,
|
|
4575
|
+
data: {
|
|
4576
|
+
plan_id,
|
|
4577
|
+
plan_name: _drive_plan(plan_id)?.name || '',
|
|
4578
|
+
changed_ts: account.drive_plan_changed || null,
|
|
4579
|
+
// The line on the invoice. Free holds no item at all.
|
|
4580
|
+
subscribed: !!account?.stripe_subscription_items?.[DRIVE_CATEGORY],
|
|
4581
|
+
used_bytes: used,
|
|
4582
|
+
quota_bytes: quota.bytes,
|
|
4583
|
+
quota_unlimited: quota.unlimited,
|
|
4584
|
+
// Where the space comes from, so a full bar can point at the plan that
|
|
4585
|
+
// would actually fix it rather than at this one by default.
|
|
4586
|
+
quota_from: {
|
|
4587
|
+
membership_bytes: Math.round((quota.breakdown.membership_gb === Infinity ? 0 : quota.breakdown.membership_gb || 0) * GB),
|
|
4588
|
+
workspace_bytes: Math.round((quota.breakdown.workspace_gb || 0) * GB),
|
|
4589
|
+
drive_plan_bytes: Math.round((quota.breakdown.drive_plan_gb || 0) * GB),
|
|
4590
|
+
},
|
|
4591
|
+
membership_plan: account.membership_plan || 'free',
|
|
4592
|
+
ai_workspace_plan: account.ai_workspace_plan || '',
|
|
4593
|
+
plans,
|
|
4594
|
+
},
|
|
4595
|
+
};
|
|
4596
|
+
} catch (err) {
|
|
4597
|
+
return { code: -1, data: err.message || String(err) };
|
|
4598
|
+
}
|
|
4599
|
+
};
|
|
4600
|
+
|
|
4601
|
+
export const drive_set_plan = async function (req) {
|
|
4602
|
+
try {
|
|
4603
|
+
const uid = req?.uid;
|
|
4604
|
+
if (!uid) return { code: -401, data: 'not authenticated' };
|
|
4605
|
+
const plan_id = req?.plan_id || '';
|
|
4606
|
+
const plan = _drive_plan(plan_id);
|
|
4607
|
+
if (!plan) return { code: -1, data: 'unknown drive plan' };
|
|
4608
|
+
|
|
4609
|
+
const account = await _drive_account_doc(uid);
|
|
4610
|
+
if (!account._id) return { code: -404, data: 'account not found' };
|
|
4611
|
+
const current = _drive_account_plan(account);
|
|
4612
|
+
if (current === plan_id) return await get_drive_plans({ uid });
|
|
4613
|
+
|
|
4614
|
+
// Downgrade guard. Taking space away from an account that is already using it
|
|
4615
|
+
// would drop it straight into metered overage on files it cannot see a way to
|
|
4616
|
+
// remove, so name the gap instead and let the customer clear it first.
|
|
4617
|
+
const target = await _drive_quota_bytes(account, plan_id);
|
|
4618
|
+
const used = _drive_used_bytes(account);
|
|
4619
|
+
if (!target.unlimited && used > target.bytes) {
|
|
4620
|
+
return {
|
|
4621
|
+
code: -1,
|
|
4622
|
+
data: `You are storing ${_drive_pretty(used)} and the ${plan.name} plan would leave you with ${_drive_pretty(target.bytes)}. Free up ${_drive_pretty(used - target.bytes)} in Drive first.`,
|
|
4623
|
+
};
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
const paid = Number(plan.price) > 0;
|
|
4627
|
+
if (paid) {
|
|
4628
|
+
const ret = await stripe_ms.add_subscription_item({ uid, plan_id });
|
|
4629
|
+
if (!ret || ret.code < 0) return { code: ret?.code === -402 || ret?.code === -3 ? -402 : -1, data: ret?.data || 'could not change the plan' };
|
|
4630
|
+
} else if (account.stripe_subscription_items?.[DRIVE_CATEGORY]) {
|
|
4631
|
+
const ret = await stripe_ms.remove_subscription_item({ uid, category: DRIVE_CATEGORY });
|
|
4632
|
+
if (!ret || ret.code < 0) return { code: -1, data: ret?.data || 'could not remove the subscription line' };
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4635
|
+
// Re-read: the stripe call just wrote stripe_subscription_items on this doc.
|
|
4636
|
+
const fresh = await _drive_account_doc(uid);
|
|
4637
|
+
fresh.drive_plan = plan_id;
|
|
4638
|
+
fresh.drive_plan_changed = Date.now();
|
|
4639
|
+
await db_module.save_couch_doc('xuda_accounts', fresh);
|
|
4640
|
+
|
|
4641
|
+
return await get_drive_plans({ uid });
|
|
4642
|
+
} catch (err) {
|
|
4643
|
+
return { code: -1, data: err.message || String(err) };
|
|
4644
|
+
}
|
|
4645
|
+
};
|
package/index_ms.mjs
CHANGED
|
@@ -197,6 +197,10 @@ export const read_user_drive_folder = async function (...args) {
|
|
|
197
197
|
return await broker.send_to_queue("read_user_drive_folder", ...args);
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
+
export const fetch_drive_file_to_tmp = async function (...args) {
|
|
201
|
+
return await broker.send_to_queue("fetch_drive_file_to_tmp", ...args);
|
|
202
|
+
};
|
|
203
|
+
|
|
200
204
|
export const put_site_files = async function (...args) {
|
|
201
205
|
return await broker.send_to_queue("put_site_files", ...args);
|
|
202
206
|
};
|
|
@@ -417,6 +421,14 @@ export const file_upload_validator = async function (...args) {
|
|
|
417
421
|
return await broker.send_to_queue("file_upload_validator", ...args);
|
|
418
422
|
};
|
|
419
423
|
|
|
424
|
+
export const get_drive_plans = async function (...args) {
|
|
425
|
+
return await broker.send_to_queue("get_drive_plans", ...args);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
export const drive_set_plan = async function (...args) {
|
|
429
|
+
return await broker.send_to_queue("drive_set_plan", ...args);
|
|
430
|
+
};
|
|
431
|
+
|
|
420
432
|
export const get_drive_files_studio = async function (...args) {
|
|
421
433
|
return await broker.send_to_queue("get_drive_files_studio", ...args);
|
|
422
434
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -197,6 +197,10 @@ export const read_user_drive_folder = function (...args) {
|
|
|
197
197
|
broker.send_to_queue_async("read_user_drive_folder", ...args);
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
+
export const fetch_drive_file_to_tmp = function (...args) {
|
|
201
|
+
broker.send_to_queue_async("fetch_drive_file_to_tmp", ...args);
|
|
202
|
+
};
|
|
203
|
+
|
|
200
204
|
export const put_site_files = function (...args) {
|
|
201
205
|
broker.send_to_queue_async("put_site_files", ...args);
|
|
202
206
|
};
|
|
@@ -417,6 +421,14 @@ export const file_upload_validator = function (...args) {
|
|
|
417
421
|
broker.send_to_queue_async("file_upload_validator", ...args);
|
|
418
422
|
};
|
|
419
423
|
|
|
424
|
+
export const get_drive_plans = function (...args) {
|
|
425
|
+
broker.send_to_queue_async("get_drive_plans", ...args);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
export const drive_set_plan = function (...args) {
|
|
429
|
+
broker.send_to_queue_async("drive_set_plan", ...args);
|
|
430
|
+
};
|
|
431
|
+
|
|
420
432
|
export const get_drive_files_studio = function (...args) {
|
|
421
433
|
broker.send_to_queue_async("get_drive_files_studio", ...args);
|
|
422
434
|
};
|