@dinoxx/dinox-cli 2.0.0 → 2.1.1

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.
Files changed (48) hide show
  1. package/dist/commands/notes/repo/read.d.ts +8 -0
  2. package/dist/commands/notes/repo/read.js +321 -0
  3. package/dist/commands/notes/repo/resolve.d.ts +8 -0
  4. package/dist/commands/notes/repo/resolve.js +187 -0
  5. package/dist/commands/notes/repo/search.d.ts +42 -0
  6. package/dist/commands/notes/repo/search.js +513 -0
  7. package/dist/commands/notes/repo/shared.d.ts +190 -0
  8. package/dist/commands/notes/repo/shared.js +102 -0
  9. package/dist/commands/notes/repo/write.d.ts +24 -0
  10. package/dist/commands/notes/repo/write.js +251 -0
  11. package/dist/commands/notes/repo.d.ts +7 -186
  12. package/dist/commands/notes/repo.js +7 -1360
  13. package/dist/commands/schema/commandMetadata.js +4 -4
  14. package/dist/commands/schema/index.d.ts +1 -0
  15. package/dist/commands/schema/index.js +23 -2
  16. package/dist/commands/schema/renderSkills.d.ts +4 -0
  17. package/dist/commands/schema/renderSkills.js +41 -2
  18. package/dist/commands/skills/index.d.ts +26 -0
  19. package/dist/commands/skills/index.js +115 -11
  20. package/dist/commands/storage/repo/configs.d.ts +30 -0
  21. package/dist/commands/storage/repo/configs.js +259 -0
  22. package/dist/commands/storage/repo/operations.d.ts +25 -0
  23. package/dist/commands/storage/repo/operations.js +337 -0
  24. package/dist/commands/storage/repo/remote.d.ts +32 -0
  25. package/dist/commands/storage/repo/remote.js +132 -0
  26. package/dist/commands/storage/repo/resources.d.ts +12 -0
  27. package/dist/commands/storage/repo/resources.js +134 -0
  28. package/dist/commands/storage/repo/shared.d.ts +181 -0
  29. package/dist/commands/storage/repo/shared.js +36 -0
  30. package/dist/commands/storage/repo/targets.d.ts +12 -0
  31. package/dist/commands/storage/repo/targets.js +178 -0
  32. package/dist/commands/storage/repo.d.ts +3 -166
  33. package/dist/commands/storage/repo.js +4 -1034
  34. package/dist/contract/snapshot.d.ts +30 -0
  35. package/dist/contract/snapshot.js +114 -0
  36. package/dist/daemon/client.js +2 -2
  37. package/dist/powersync/uploader.js +27 -2
  38. package/dist/skills/commandRefs.d.ts +21 -0
  39. package/dist/skills/commandRefs.js +132 -0
  40. package/dist/skills/generate.d.ts +13 -0
  41. package/dist/skills/generate.js +80 -0
  42. package/dist/skills/registry.d.ts +13 -0
  43. package/dist/skills/registry.js +58 -0
  44. package/dist/utils/errorCodes.d.ts +311 -0
  45. package/dist/utils/errorCodes.js +92 -0
  46. package/dist/utils/errors.d.ts +13 -2
  47. package/dist/utils/errors.js +11 -0
  48. package/package.json +11 -4
@@ -0,0 +1,132 @@
1
+ // S3 object operations, collision/unknown-outcome errors, and thumbnail rendering.
2
+ import fs from 'node:fs';
3
+ import { DinoxError } from '../../../utils/errors.js';
4
+ import { normalizeText, } from './shared.js';
5
+ import { finishStorageHost } from './targets.js';
6
+ export async function defaultUploadObject(input) {
7
+ const { PutObjectCommand, S3Client } = await import('@aws-sdk/client-s3');
8
+ const client = new S3Client({
9
+ endpoint: finishStorageHost(input.storage.endpoint),
10
+ region: input.storage.region,
11
+ forcePathStyle: input.storage.forcePathStyle,
12
+ credentials: {
13
+ accessKeyId: input.storage.accessKeyId,
14
+ secretAccessKey: input.storage.secretAccessKey,
15
+ },
16
+ });
17
+ const response = await client.send(new PutObjectCommand({
18
+ Bucket: input.bucket,
19
+ Key: input.key,
20
+ Body: fs.createReadStream(input.localPath),
21
+ ContentType: input.contentType,
22
+ ContentLength: input.contentLength,
23
+ IfNoneMatch: input.overwrite ? undefined : '*',
24
+ }));
25
+ return {
26
+ etag: normalizeText(response.ETag) || null,
27
+ };
28
+ }
29
+ export function isObjectCollisionError(error) {
30
+ if (!error || typeof error !== 'object') {
31
+ return false;
32
+ }
33
+ const record = error;
34
+ return record.$metadata?.httpStatusCode === 409
35
+ || record.$metadata?.httpStatusCode === 412
36
+ || record.name === 'ConditionalRequestConflict'
37
+ || record.name === 'PreconditionFailed'
38
+ || record.Code === 'PreconditionFailed'
39
+ || record.code === 'PreconditionFailed';
40
+ }
41
+ export function createStorageCollisionError(bucket, keys, failureDetails = {}) {
42
+ return new DinoxError(`Storage object already exists: ${keys.join(', ')}`, {
43
+ code: 'STORAGE_OBJECT_ALREADY_EXISTS',
44
+ suggestion: 'Choose a different --key, or pass --overwrite to replace the existing object.',
45
+ recoverable: true,
46
+ details: {
47
+ bucket,
48
+ keys,
49
+ ...failureDetails,
50
+ },
51
+ });
52
+ }
53
+ export function createStorageUploadOutcomeUnknownError(input) {
54
+ return new DinoxError(input.message, {
55
+ cause: input.cause,
56
+ code: 'STORAGE_UPLOAD_OUTCOME_UNKNOWN',
57
+ recoverable: true,
58
+ suggestion: 'Inspect the reported object key in remote storage before retrying; do not assume the failed request left the object unchanged.',
59
+ details: {
60
+ bucket: input.bucket,
61
+ storageKey: input.storageKey,
62
+ operation: input.operation,
63
+ overwrite: input.overwrite,
64
+ remoteOutcome: 'unknown',
65
+ remoteObjectChanged: null,
66
+ affectedKeys: [],
67
+ possiblyAffectedKeys: [...new Set(input.possiblyAffectedKeys)],
68
+ },
69
+ });
70
+ }
71
+ export async function defaultDeleteObject(input) {
72
+ const { DeleteObjectCommand, S3Client } = await import('@aws-sdk/client-s3');
73
+ const client = new S3Client({
74
+ endpoint: finishStorageHost(input.storage.endpoint),
75
+ region: input.storage.region,
76
+ forcePathStyle: input.storage.forcePathStyle,
77
+ credentials: {
78
+ accessKeyId: input.storage.accessKeyId,
79
+ secretAccessKey: input.storage.secretAccessKey,
80
+ },
81
+ });
82
+ await client.send(new DeleteObjectCommand({
83
+ Bucket: input.bucket,
84
+ Key: input.key,
85
+ IfMatch: normalizeText(input.etag) || undefined,
86
+ }));
87
+ }
88
+ export async function defaultUploadBytes(input) {
89
+ const { PutObjectCommand, S3Client } = await import('@aws-sdk/client-s3');
90
+ const client = new S3Client({
91
+ endpoint: finishStorageHost(input.storage.endpoint),
92
+ region: input.storage.region,
93
+ forcePathStyle: input.storage.forcePathStyle,
94
+ credentials: {
95
+ accessKeyId: input.storage.accessKeyId,
96
+ secretAccessKey: input.storage.secretAccessKey,
97
+ },
98
+ });
99
+ const response = await client.send(new PutObjectCommand({
100
+ Bucket: input.bucket,
101
+ Key: input.key,
102
+ Body: input.body,
103
+ ContentType: input.contentType,
104
+ ContentLength: input.body.byteLength,
105
+ IfNoneMatch: input.overwrite ? undefined : '*',
106
+ }));
107
+ return {
108
+ etag: normalizeText(response.ETag) || null,
109
+ };
110
+ }
111
+ // sharp is an optional native dependency: when it failed to install, the rest of the CLI
112
+ // still works and only image uploads (which require a thumbnail) are refused up front.
113
+ export async function loadSharp() {
114
+ try {
115
+ const { default: sharp } = await import('sharp');
116
+ return sharp;
117
+ }
118
+ catch (error) {
119
+ throw new DinoxError('Image uploads need the optional `sharp` image library, which is not installed or failed to load.', {
120
+ code: 'STORAGE_IMAGE_PROCESSING_UNAVAILABLE',
121
+ cause: error,
122
+ suggestion: 'Reinstall the CLI with optional dependencies enabled (for example `npm install -g @dinoxx/dinox-cli --include=optional`), then retry the upload.',
123
+ recoverable: true,
124
+ });
125
+ }
126
+ }
127
+ export async function generateThumbnailBuffer(sharp, localPath) {
128
+ return sharp(localPath)
129
+ .resize({ width: 400, withoutEnlargement: true })
130
+ .webp({ quality: 50 })
131
+ .toBuffer();
132
+ }
@@ -0,0 +1,12 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import { type StorageUploadPlan, type StoredCustomStorageConfig } from './shared.js';
3
+ type UploadedResourceWriteArgs = {
4
+ userId: string;
5
+ storage: StoredCustomStorageConfig;
6
+ plan: StorageUploadPlan;
7
+ etag: string | null;
8
+ thumbnailUrl: string | null;
9
+ overwrite: boolean;
10
+ };
11
+ export declare function persistUploadedResource(db: AbstractPowerSyncDatabase, args: UploadedResourceWriteArgs): Promise<string>;
12
+ export {};
@@ -0,0 +1,134 @@
1
+ // Recording uploaded objects as c_resource rows.
2
+ import { withPowerSyncWriteTransaction } from '../../../powersync/runtime.js';
3
+ import { generateId } from '../../../utils/id.js';
4
+ import { DinoxError } from '../../../utils/errors.js';
5
+ import { normalizeText, } from './shared.js';
6
+ import { getTableColumns } from './configs.js';
7
+ function buildUploadedResourceEntries(columns, args, id, now) {
8
+ const entries = [];
9
+ const push = (column, value) => {
10
+ if (columns.has(column)) {
11
+ entries.push([column, value]);
12
+ }
13
+ };
14
+ push('id', id);
15
+ push('user_id', args.userId);
16
+ push('name', args.plan.fileName);
17
+ push('mime_type', args.plan.contentType);
18
+ push('size_bytes', args.plan.sizeBytes);
19
+ push('storage_config_id', args.storage.id);
20
+ push('storage_key', args.plan.storageKey);
21
+ push('storage_url', args.plan.storageUrl);
22
+ push('storage_provider', args.storage.provider);
23
+ push('bucket', args.storage.bucket);
24
+ push('is_public', args.storage.isPrivate ? 0 : 1);
25
+ push('created_at', now);
26
+ push('updated_at', now);
27
+ push('is_del', 0);
28
+ push('status', 'uploaded');
29
+ push('uploaded_at', now);
30
+ push('etag', args.etag);
31
+ push('checksum', null);
32
+ push('category', args.plan.category);
33
+ push('endpoint', args.storage.endpoint);
34
+ push('domain', args.plan.domain);
35
+ push('extra_data', '{}');
36
+ push('deleted_at', null);
37
+ push('content_sha256', null);
38
+ push('content', null);
39
+ push('content_md', null);
40
+ push('summary', null);
41
+ return entries;
42
+ }
43
+ async function insertResourceRow(db, entries) {
44
+ const insertColumns = entries.map(([column]) => column);
45
+ const placeholders = insertColumns.map(() => '?').join(', ');
46
+ await db.execute(`
47
+ INSERT INTO c_resource (${insertColumns.join(', ')})
48
+ VALUES (${placeholders})
49
+ `, entries.map(([, value]) => value));
50
+ }
51
+ export async function persistUploadedResource(db, args) {
52
+ const columns = await getTableColumns(db, 'c_resource');
53
+ const requiredColumns = [
54
+ 'id',
55
+ 'user_id',
56
+ 'name',
57
+ 'mime_type',
58
+ 'size_bytes',
59
+ 'storage_key',
60
+ 'storage_url',
61
+ 'storage_provider',
62
+ 'bucket',
63
+ 'created_at',
64
+ 'updated_at',
65
+ 'is_del',
66
+ ];
67
+ const missingRequired = requiredColumns.filter((column) => !columns.has(column));
68
+ if (missingRequired.length > 0) {
69
+ throw new DinoxError(`c_resource table is missing required columns: ${missingRequired.join(', ')}`);
70
+ }
71
+ const id = generateId();
72
+ const now = new Date().toISOString();
73
+ const entries = buildUploadedResourceEntries(columns, args, id, now);
74
+ if (!args.overwrite) {
75
+ await insertResourceRow(db, entries);
76
+ return id;
77
+ }
78
+ return withPowerSyncWriteTransaction(db, async (tx) => {
79
+ const identityClauses = [
80
+ 'user_id = ?',
81
+ 'storage_key = ?',
82
+ 'COALESCE(is_del, 0) = 0',
83
+ ];
84
+ const identityParams = [args.userId, args.plan.storageKey];
85
+ if (columns.has('storage_config_id')) {
86
+ identityClauses.push('storage_config_id = ?');
87
+ identityParams.push(args.storage.id);
88
+ }
89
+ else {
90
+ identityClauses.push('storage_provider = ?', 'bucket = ?');
91
+ identityParams.push(args.storage.provider, args.storage.bucket);
92
+ if (columns.has('endpoint')) {
93
+ identityClauses.push('endpoint = ?');
94
+ identityParams.push(args.storage.endpoint);
95
+ }
96
+ }
97
+ const existingRows = await tx.getAll(`
98
+ SELECT id
99
+ FROM c_resource
100
+ WHERE ${identityClauses.join('\n AND ')}
101
+ ORDER BY created_at ASC, id ASC
102
+ `, identityParams);
103
+ const existingIds = [...new Set(existingRows.map((row) => normalizeText(row.id)).filter(Boolean))];
104
+ const existingId = existingIds[0];
105
+ if (!existingId) {
106
+ await insertResourceRow(tx, entries);
107
+ return id;
108
+ }
109
+ const updateEntries = entries.filter(([column]) => (column !== 'id' && column !== 'user_id' && column !== 'created_at'));
110
+ await tx.execute(`
111
+ UPDATE c_resource
112
+ SET ${updateEntries.map(([column]) => `${column} = ?`).join(', ')}
113
+ WHERE id = ?
114
+ AND user_id = ?
115
+ AND COALESCE(is_del, 0) = 0
116
+ `, [...updateEntries.map(([, value]) => value), existingId, args.userId]);
117
+ const duplicateIds = existingIds.slice(1);
118
+ if (duplicateIds.length > 0) {
119
+ const duplicateUpdates = [
120
+ ['updated_at', now],
121
+ ['is_del', 1],
122
+ ...(columns.has('deleted_at') ? [['deleted_at', now]] : []),
123
+ ];
124
+ await tx.execute(`
125
+ UPDATE c_resource
126
+ SET ${duplicateUpdates.map(([column]) => `${column} = ?`).join(', ')}
127
+ WHERE user_id = ?
128
+ AND COALESCE(is_del, 0) = 0
129
+ AND id IN (${duplicateIds.map(() => '?').join(', ')})
130
+ `, [...duplicateUpdates.map(([, value]) => value), args.userId, ...duplicateIds]);
131
+ }
132
+ return existingId;
133
+ });
134
+ }
@@ -0,0 +1,181 @@
1
+ import fsp from 'node:fs/promises';
2
+ export declare const ACTIVE_CUSTOM_STORAGE_CONFIG_ID_KEY = "storage:active_custom_config_id";
3
+ export declare const DEFAULT_STORAGE_PROTOCOL = "s3";
4
+ export declare const DEFAULT_STORAGE_PATH_PREFIX = "dinox_note/";
5
+ export declare const DEFAULT_CONTENT_TYPE = "application/octet-stream";
6
+ export declare const OFFICIAL_STORAGE_PROVIDER = "aliyun-official";
7
+ export declare const OFFICIAL_STORAGE_PROVIDER_NAME = "\u5B98\u65B9\u4E91";
8
+ export declare const UPLOAD_CATEGORY_VALUES: readonly ["images", "audios", "files", "videos"];
9
+ export type StorageUploadCategory = (typeof UPLOAD_CATEGORY_VALUES)[number];
10
+ export type StorageListItem = {
11
+ id: string;
12
+ aliasName: string;
13
+ provider: string;
14
+ endpoint: string;
15
+ bucket: string;
16
+ region: string;
17
+ forcePathStyle: boolean;
18
+ pathPrefix: string;
19
+ customDomain: string;
20
+ legacyDomains: string[];
21
+ isPrivate: boolean;
22
+ hasAccessKeyId: boolean;
23
+ hasSecretAccessKey: boolean;
24
+ isActive: boolean;
25
+ };
26
+ export type StorageListResult = {
27
+ activeCustomStorageConfigId: string | null;
28
+ usesOfficialStorage: boolean;
29
+ items: StorageListItem[];
30
+ };
31
+ export type StorageRow = {
32
+ id: string | null;
33
+ alias_name: string | null;
34
+ protocol: string | null;
35
+ provider: string | null;
36
+ endpoint: string | null;
37
+ bucket: string | null;
38
+ region: string | null;
39
+ access_key_id: string | null;
40
+ secret_access_key: string | null;
41
+ force_path_style: number | null;
42
+ path_prefix: string | null;
43
+ custom_domain: string | null;
44
+ legacy_domains: string | null;
45
+ is_private: number | null;
46
+ updated_at: string | null;
47
+ deleted_at: string | null;
48
+ };
49
+ export type ConfigPropertyRow = {
50
+ c_value: string | null;
51
+ };
52
+ export type TableInfoRow = {
53
+ name: string;
54
+ };
55
+ export type StoredCustomStorageConfig = {
56
+ id: string;
57
+ aliasName: string;
58
+ provider: string;
59
+ endpoint: string;
60
+ bucket: string;
61
+ region: string;
62
+ accessKeyId: string;
63
+ secretAccessKey: string;
64
+ forcePathStyle: boolean;
65
+ pathPrefix: string;
66
+ customDomain: string;
67
+ legacyDomains: string[];
68
+ isPrivate: boolean;
69
+ };
70
+ export type StorageUploadPlan = {
71
+ storage: StorageListItem;
72
+ selectionSource: 'storage_id' | 'active' | 'single_available_config';
73
+ filePath: string;
74
+ fileName: string;
75
+ sizeBytes: number;
76
+ category: StorageUploadCategory;
77
+ contentType: string;
78
+ storageKey: string;
79
+ storageUrl: string;
80
+ domain: string;
81
+ };
82
+ export type StorageUploadResult = {
83
+ resourceId: string;
84
+ storageConfigId: string;
85
+ aliasName: string;
86
+ provider: string;
87
+ bucket: string;
88
+ endpoint: string;
89
+ domain: string;
90
+ storageKey: string;
91
+ storageUrl: string;
92
+ fileName: string;
93
+ sizeBytes: number;
94
+ contentType: string;
95
+ category: StorageUploadCategory;
96
+ etag: string | null;
97
+ thumbnailUrl: string | null;
98
+ isPrivate: boolean;
99
+ selectionSource: StorageUploadPlan['selectionSource'];
100
+ };
101
+ export type StorageTestPlan = {
102
+ storage: StorageListItem;
103
+ selectionSource: StorageUploadPlan['selectionSource'];
104
+ contentType: string;
105
+ sizeBytes: number;
106
+ storageKey: string;
107
+ storageUrl: string;
108
+ domain: string;
109
+ };
110
+ export type StorageTestResult = {
111
+ storageConfigId: string;
112
+ aliasName: string;
113
+ provider: string;
114
+ bucket: string;
115
+ endpoint: string;
116
+ domain: string;
117
+ storageKey: string;
118
+ storageUrl: string;
119
+ contentType: string;
120
+ sizeBytes: number;
121
+ selectionSource: StorageUploadPlan['selectionSource'];
122
+ isPrivate: boolean;
123
+ };
124
+ export type StorageStatsEntry = {
125
+ provider: string;
126
+ bucket: string;
127
+ label: string;
128
+ totalBytes: number;
129
+ fileCount: number;
130
+ storageConfigId: string | null;
131
+ aliasName: string | null;
132
+ isOfficial: boolean;
133
+ };
134
+ export type StorageStatsResult = {
135
+ totalBytes: number;
136
+ totalFiles: number;
137
+ entries: StorageStatsEntry[];
138
+ };
139
+ export type UploadObjectInput = {
140
+ localPath: string;
141
+ bucket: string;
142
+ key: string;
143
+ contentType: string;
144
+ contentLength: number;
145
+ overwrite: boolean;
146
+ storage: StoredCustomStorageConfig;
147
+ };
148
+ export type UploadObjectResult = {
149
+ etag: string | null;
150
+ };
151
+ export type UploadDeps = {
152
+ statFile?: typeof fsp.stat;
153
+ uploadObject?: (input: UploadObjectInput) => Promise<UploadObjectResult>;
154
+ deleteObject?: (input: {
155
+ bucket: string;
156
+ key: string;
157
+ etag?: string | null;
158
+ storage: StoredCustomStorageConfig;
159
+ }) => Promise<void>;
160
+ uploadBytes?: (input: {
161
+ body: Buffer;
162
+ bucket: string;
163
+ key: string;
164
+ contentType: string;
165
+ overwrite?: boolean;
166
+ storage: StoredCustomStorageConfig;
167
+ }) => Promise<UploadObjectResult>;
168
+ };
169
+ export type StorageStatsRow = {
170
+ provider: string | null;
171
+ bucket: string | null;
172
+ total_bytes: number | string | null;
173
+ file_count: number | string | null;
174
+ };
175
+ export type ActiveResourceRow = {
176
+ id: string | null;
177
+ };
178
+ export declare function normalizeText(value: unknown): string;
179
+ export declare function normalizeTextArray(raw: string | null): string[];
180
+ export declare function normalizeBool(value: number | null): boolean;
181
+ export declare function normalizeNumber(value: unknown): number;
@@ -0,0 +1,36 @@
1
+ // Constants, row/result types, and value normalizers shared by the storage modules.
2
+ export const ACTIVE_CUSTOM_STORAGE_CONFIG_ID_KEY = 'storage:active_custom_config_id';
3
+ export const DEFAULT_STORAGE_PROTOCOL = 's3';
4
+ export const DEFAULT_STORAGE_PATH_PREFIX = 'dinox_note/';
5
+ export const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
6
+ export const OFFICIAL_STORAGE_PROVIDER = 'aliyun-official';
7
+ export const OFFICIAL_STORAGE_PROVIDER_NAME = '官方云';
8
+ export const UPLOAD_CATEGORY_VALUES = ['images', 'audios', 'files', 'videos'];
9
+ export function normalizeText(value) {
10
+ return typeof value === 'string' ? value.trim() : '';
11
+ }
12
+ export function normalizeTextArray(raw) {
13
+ const input = normalizeText(raw);
14
+ if (!input) {
15
+ return [];
16
+ }
17
+ try {
18
+ const parsed = JSON.parse(input);
19
+ if (!Array.isArray(parsed)) {
20
+ return [];
21
+ }
22
+ return parsed
23
+ .map((entry) => normalizeText(String(entry)))
24
+ .filter(Boolean);
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ }
30
+ export function normalizeBool(value) {
31
+ return Number(value ?? 0) === 1;
32
+ }
33
+ export function normalizeNumber(value) {
34
+ const numeric = typeof value === 'number' ? value : Number(value ?? 0);
35
+ return Number.isFinite(numeric) ? numeric : 0;
36
+ }
@@ -0,0 +1,12 @@
1
+ import { type StorageUploadCategory, type StoredCustomStorageConfig } from './shared.js';
2
+ export declare function normalizeStoragePathPrefix(value: string): string;
3
+ export declare function finishStorageHost(value: string): string;
4
+ export declare function guessContentType(filePath: string): string;
5
+ export declare function buildObjectId(userId: string, category: StorageUploadCategory, filePath: string): string;
6
+ export declare function generateThumbnailKey(originalKey: string): string;
7
+ export declare function buildStorageTarget(config: StoredCustomStorageConfig, key: string): {
8
+ storageUrl: string;
9
+ domain: string;
10
+ };
11
+ export declare function isOfficialStorageProvider(provider: string): boolean;
12
+ export declare function getStorageProviderName(provider: string): string;
@@ -0,0 +1,178 @@
1
+ // Object keys, public URLs, content types, and provider naming for storage targets.
2
+ import path from 'node:path';
3
+ import { generateId } from '../../../utils/id.js';
4
+ import { DinoxError } from '../../../utils/errors.js';
5
+ import { DEFAULT_CONTENT_TYPE, DEFAULT_STORAGE_PATH_PREFIX, OFFICIAL_STORAGE_PROVIDER, OFFICIAL_STORAGE_PROVIDER_NAME, } from './shared.js';
6
+ export function normalizeStoragePathPrefix(value) {
7
+ const trimmed = value.trim();
8
+ if (!trimmed) {
9
+ return DEFAULT_STORAGE_PATH_PREFIX;
10
+ }
11
+ return trimmed.endsWith('/') ? trimmed : `${trimmed}/`;
12
+ }
13
+ export function finishStorageHost(value) {
14
+ const trimmed = value.trim();
15
+ if (!trimmed) {
16
+ throw new DinoxError('Storage endpoint is required.');
17
+ }
18
+ const candidate = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
19
+ try {
20
+ const parsed = new URL(candidate);
21
+ parsed.pathname = '';
22
+ parsed.search = '';
23
+ parsed.hash = '';
24
+ return parsed.toString().replace(/\/$/, '');
25
+ }
26
+ catch (error) {
27
+ throw new DinoxError(`Invalid storage endpoint: ${trimmed}`, { cause: error });
28
+ }
29
+ }
30
+ function encodeStorageKeyForUrl(key) {
31
+ return key
32
+ .split('/')
33
+ .map((segment) => {
34
+ return encodeURIComponent(segment).replace(/[!'()*]/g, (character) => {
35
+ return `%${character.charCodeAt(0).toString(16).toUpperCase()}`;
36
+ });
37
+ })
38
+ .join('/');
39
+ }
40
+ function assertStorageKeyUrlSafe(key) {
41
+ const unsafeSegment = key.split('/').find((segment) => segment === '.' || segment === '..');
42
+ if (!unsafeSegment) {
43
+ return;
44
+ }
45
+ throw new DinoxError('Storage object key must not contain "." or ".." path segments.', {
46
+ code: 'INVALID_STORAGE_KEY',
47
+ suggestion: 'Remove dot-only path segments from --key or the configured storage path prefix.',
48
+ details: { key },
49
+ });
50
+ }
51
+ function buildVirtualHostedStyleUrl(bucket, endpoint, key) {
52
+ const parsed = new URL(endpoint);
53
+ const protocol = parsed.protocol || 'https:';
54
+ const port = parsed.port ? `:${parsed.port}` : '';
55
+ const host = parsed.hostname || parsed.host;
56
+ const hostWithBucket = host.startsWith(`${bucket}.`) ? host : `${bucket}.${host}`;
57
+ const encodedKey = encodeStorageKeyForUrl(key);
58
+ return {
59
+ finalUrl: `${protocol}//${hostWithBucket}${port}/${encodedKey}`,
60
+ domain: `${protocol}//${hostWithBucket}${port}`,
61
+ };
62
+ }
63
+ function buildPathStyleUrl(bucket, endpoint, key) {
64
+ const parsed = new URL(endpoint);
65
+ const protocol = parsed.protocol || 'https:';
66
+ const port = parsed.port ? `:${parsed.port}` : '';
67
+ const host = parsed.hostname || parsed.host;
68
+ const encodedKey = encodeStorageKeyForUrl(key);
69
+ return {
70
+ finalUrl: `${protocol}//${host}${port}/${bucket}/${encodedKey}`,
71
+ domain: `${protocol}//${host}${port}`,
72
+ };
73
+ }
74
+ function getFileExtension(filePath) {
75
+ const extension = path.extname(filePath).trim().replace(/^\./, '');
76
+ return extension.toLowerCase();
77
+ }
78
+ export function guessContentType(filePath) {
79
+ const extension = getFileExtension(filePath);
80
+ switch (extension) {
81
+ case 'jpg':
82
+ case 'jpeg':
83
+ return 'image/jpeg';
84
+ case 'png':
85
+ return 'image/png';
86
+ case 'gif':
87
+ return 'image/gif';
88
+ case 'webp':
89
+ return 'image/webp';
90
+ case 'svg':
91
+ return 'image/svg+xml';
92
+ case 'mp3':
93
+ return 'audio/mpeg';
94
+ case 'm4a':
95
+ return 'audio/mp4';
96
+ case 'wav':
97
+ return 'audio/wav';
98
+ case 'ogg':
99
+ case 'opus':
100
+ return 'audio/ogg';
101
+ case 'mp4':
102
+ return 'video/mp4';
103
+ case 'mov':
104
+ return 'video/quicktime';
105
+ case 'webm':
106
+ return 'video/webm';
107
+ case 'pdf':
108
+ return 'application/pdf';
109
+ case 'json':
110
+ return 'application/json';
111
+ case 'md':
112
+ return 'text/markdown';
113
+ case 'txt':
114
+ return 'text/plain';
115
+ case 'csv':
116
+ return 'text/csv';
117
+ default:
118
+ return DEFAULT_CONTENT_TYPE;
119
+ }
120
+ }
121
+ export function buildObjectId(userId, category, filePath) {
122
+ const ext = getFileExtension(filePath);
123
+ const suffix = ext ? `.${ext}` : '';
124
+ return `assets/${userId}/${category}/${generateId()}${suffix}`;
125
+ }
126
+ export function generateThumbnailKey(originalKey) {
127
+ const lastSlashIndex = originalKey.lastIndexOf('/');
128
+ if (lastSlashIndex === -1) {
129
+ const filenameWithoutExt = originalKey.replace(/\.[^.]+$/, '');
130
+ return `thumb/${filenameWithoutExt}_thumbnail.webp`;
131
+ }
132
+ const dirPath = originalKey.slice(0, lastSlashIndex);
133
+ const filename = originalKey.slice(lastSlashIndex + 1);
134
+ const filenameWithoutExt = filename.replace(/\.[^.]+$/, '');
135
+ return `${dirPath}/thumb/${filenameWithoutExt}_thumbnail.webp`;
136
+ }
137
+ export function buildStorageTarget(config, key) {
138
+ assertStorageKeyUrlSafe(key);
139
+ const resolvedEndpoint = finishStorageHost(config.endpoint);
140
+ if (config.customDomain) {
141
+ const domain = finishStorageHost(config.customDomain);
142
+ return {
143
+ storageUrl: `${domain}/${encodeStorageKeyForUrl(key)}`,
144
+ domain,
145
+ };
146
+ }
147
+ const target = config.forcePathStyle
148
+ ? buildPathStyleUrl(config.bucket, resolvedEndpoint, key)
149
+ : buildVirtualHostedStyleUrl(config.bucket, resolvedEndpoint, key);
150
+ return {
151
+ storageUrl: target.finalUrl,
152
+ domain: target.domain,
153
+ };
154
+ }
155
+ export function isOfficialStorageProvider(provider) {
156
+ return provider === OFFICIAL_STORAGE_PROVIDER;
157
+ }
158
+ export function getStorageProviderName(provider) {
159
+ if (isOfficialStorageProvider(provider)) {
160
+ return OFFICIAL_STORAGE_PROVIDER_NAME;
161
+ }
162
+ switch (provider) {
163
+ case 'bitiful':
164
+ return '缤纷云';
165
+ case 'tencent':
166
+ return '腾讯云';
167
+ case 'aliyun':
168
+ return '阿里云';
169
+ case 'aws':
170
+ return 'AWS';
171
+ case 'qiniu':
172
+ return '七牛云';
173
+ case 'cloudflare':
174
+ return 'Cloudflare';
175
+ default:
176
+ return provider;
177
+ }
178
+ }