@everystack/cli 0.1.0
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/README.md +255 -0
- package/package.json +104 -0
- package/src/cli/aws.ts +121 -0
- package/src/cli/commands/analyze.ts +61 -0
- package/src/cli/commands/branches.ts +97 -0
- package/src/cli/commands/cache.ts +72 -0
- package/src/cli/commands/certs.ts +117 -0
- package/src/cli/commands/channels.ts +109 -0
- package/src/cli/commands/console.ts +68 -0
- package/src/cli/commands/db.ts +183 -0
- package/src/cli/commands/diag.ts +242 -0
- package/src/cli/commands/logs.ts +282 -0
- package/src/cli/commands/update.ts +432 -0
- package/src/cli/config.ts +98 -0
- package/src/cli/discover.ts +321 -0
- package/src/cli/hydration-analyzer.ts +224 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/output.ts +25 -0
- package/src/cli/ssr-analyzer.ts +445 -0
- package/src/cli/utils/export.ts +8 -0
- package/src/cli/utils/table.ts +39 -0
- package/src/cli/utils/upload.ts +52 -0
- package/src/cli/utils/walk.ts +59 -0
- package/src/client/app-state-provider.tsx +83 -0
- package/src/client/index.ts +2 -0
- package/src/client/updates-provider.tsx +69 -0
- package/src/handler/assets.ts +30 -0
- package/src/handler/branches.ts +70 -0
- package/src/handler/channels-crud.ts +174 -0
- package/src/handler/helpers.ts +239 -0
- package/src/handler/index.ts +78 -0
- package/src/handler/manifest.ts +276 -0
- package/src/handler/multipart.ts +74 -0
- package/src/handler/publish-web.ts +311 -0
- package/src/handler/publish.ts +346 -0
- package/src/handler/signing.ts +29 -0
- package/src/handler/types.ts +16 -0
- package/src/index.ts +4 -0
- package/src/schema.ts +245 -0
- package/src/storage/filesystem.ts +103 -0
- package/src/storage/index.ts +27 -0
- package/src/storage/s3.ts +125 -0
package/src/schema.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI schema — everyschema ops.opsItems + ops.buildArtifacts + ops.runs
|
|
3
|
+
*
|
|
4
|
+
* OTA update pipeline mapped to everyschema:
|
|
5
|
+
* updateBranches → eliminated (branch name in runs.data.branchRef)
|
|
6
|
+
* updateChannels → opsItems (type='release_channel')
|
|
7
|
+
* updateGroups → runs (type='release')
|
|
8
|
+
* updateReleases → buildArtifacts (type='ota')
|
|
9
|
+
* updateAssets → buildArtifacts (type='ota_asset')
|
|
10
|
+
*
|
|
11
|
+
* All table definitions copied locally to avoid TS6059 rootDir issues
|
|
12
|
+
* with cross-package re-exports during ts-jest compilation.
|
|
13
|
+
*
|
|
14
|
+
* @see everyschema — canonical schema definitions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
pgSchema,
|
|
19
|
+
text,
|
|
20
|
+
timestamp,
|
|
21
|
+
uuid,
|
|
22
|
+
integer,
|
|
23
|
+
bigint,
|
|
24
|
+
index,
|
|
25
|
+
uniqueIndex,
|
|
26
|
+
jsonb,
|
|
27
|
+
} from 'drizzle-orm/pg-core';
|
|
28
|
+
import { sql } from 'drizzle-orm';
|
|
29
|
+
|
|
30
|
+
const opsSchema = pgSchema('ops');
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// RUNS (STI: script | test | job | worker | release)
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Copied from @everystack/jobs/schema to avoid TS6059 rootDir errors.
|
|
38
|
+
* Canonical definition lives in jobs package.
|
|
39
|
+
*/
|
|
40
|
+
export const runs = opsSchema.table(
|
|
41
|
+
'runs',
|
|
42
|
+
{
|
|
43
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
44
|
+
type: text('type').notNull(), // 'script' | 'test' | 'job' | 'worker' | 'release'
|
|
45
|
+
repositoryId: uuid('repository_id'),
|
|
46
|
+
userId: uuid('user_id'),
|
|
47
|
+
accountId: uuid('account_id'),
|
|
48
|
+
|
|
49
|
+
// Naming
|
|
50
|
+
name: text('name'),
|
|
51
|
+
|
|
52
|
+
// Status
|
|
53
|
+
status: text('status').notNull(),
|
|
54
|
+
priority: integer('priority').default(0),
|
|
55
|
+
exitCode: integer('exit_code'),
|
|
56
|
+
errorMessage: text('error_message'),
|
|
57
|
+
|
|
58
|
+
// Output
|
|
59
|
+
stdout: text('stdout'),
|
|
60
|
+
stderr: text('stderr'),
|
|
61
|
+
|
|
62
|
+
// Duration
|
|
63
|
+
durationMs: integer('duration_ms'),
|
|
64
|
+
|
|
65
|
+
// Lifecycle
|
|
66
|
+
queuedAt: timestamp('queued_at', { withTimezone: true }).notNull(),
|
|
67
|
+
startedAt: timestamp('started_at', { withTimezone: true }),
|
|
68
|
+
completedAt: timestamp('completed_at', { withTimezone: true }),
|
|
69
|
+
timeoutMs: integer('timeout_ms'),
|
|
70
|
+
|
|
71
|
+
// Context
|
|
72
|
+
triggeredBy: text('triggered_by'),
|
|
73
|
+
sessionId: text('session_id'),
|
|
74
|
+
|
|
75
|
+
// DAG support (parallel builds)
|
|
76
|
+
parentId: uuid('parent_id'),
|
|
77
|
+
dependsOn: uuid('depends_on').array(),
|
|
78
|
+
matrix: jsonb('matrix').$type<Record<string, string>>(),
|
|
79
|
+
|
|
80
|
+
// Type-specific data
|
|
81
|
+
data: jsonb('data'),
|
|
82
|
+
},
|
|
83
|
+
(table) => ({
|
|
84
|
+
typeIdx: index('runs_type_idx').on(table.type),
|
|
85
|
+
statusIdx: index('runs_status_idx').on(table.status),
|
|
86
|
+
repoIdx: index('runs_repo_idx').on(table.repositoryId),
|
|
87
|
+
typeStatusIdx: index('runs_type_status_idx').on(table.type, table.status),
|
|
88
|
+
queuedIdx: index('runs_queued_idx').on(table.queuedAt),
|
|
89
|
+
sessionIdx: index('runs_session_idx').on(table.sessionId),
|
|
90
|
+
accountIdx: index('runs_account_idx').on(table.accountId),
|
|
91
|
+
parentIdx: index('runs_parent_idx').on(table.parentId),
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// OPS ITEMS (STI: certificate | domain_verification | acme_account | release_channel)
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Infrastructure and operational state items using Single Table Inheritance.
|
|
101
|
+
*
|
|
102
|
+
* type='release_channel': OTA/deploy channel → branch routing
|
|
103
|
+
* - name: channel identifier (e.g. 'production', 'staging')
|
|
104
|
+
* - data: { branchRef }
|
|
105
|
+
*/
|
|
106
|
+
export const opsItems = opsSchema.table(
|
|
107
|
+
'items',
|
|
108
|
+
{
|
|
109
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
110
|
+
type: text('type').notNull(), // certificate | domain_verification | acme_account | release_channel
|
|
111
|
+
|
|
112
|
+
// Ownership
|
|
113
|
+
tenantId: uuid('tenant_id'),
|
|
114
|
+
repositoryId: uuid('repository_id'),
|
|
115
|
+
|
|
116
|
+
// Shared columns
|
|
117
|
+
name: text('name'),
|
|
118
|
+
status: text('status').notNull().default('pending'),
|
|
119
|
+
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
|
120
|
+
|
|
121
|
+
// Crypto reference
|
|
122
|
+
secretId: uuid('secret_id'),
|
|
123
|
+
|
|
124
|
+
// Promoted for indexing (queried on every TLS handshake)
|
|
125
|
+
domainPattern: text('domain_pattern'),
|
|
126
|
+
|
|
127
|
+
// Type-specific data
|
|
128
|
+
data: jsonb('data'),
|
|
129
|
+
|
|
130
|
+
// Lifecycle
|
|
131
|
+
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
132
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
133
|
+
},
|
|
134
|
+
(table) => ({
|
|
135
|
+
typeIdx: index('items_type_idx').on(table.type),
|
|
136
|
+
nameIdx: index('items_name_idx').on(table.name),
|
|
137
|
+
statusIdx: index('items_status_idx').on(table.status),
|
|
138
|
+
tenantIdx: index('items_tenant_idx').on(table.tenantId),
|
|
139
|
+
repoIdx: index('items_repo_idx').on(table.repositoryId),
|
|
140
|
+
expiresIdx: index('items_expires_idx').on(table.expiresAt),
|
|
141
|
+
domainPatternIdx: index('items_domain_pattern_idx')
|
|
142
|
+
.on(table.domainPattern)
|
|
143
|
+
.where(sql`${table.type} = 'certificate'`),
|
|
144
|
+
certSyncIdx: index('items_cert_sync_idx')
|
|
145
|
+
.on(table.status, table.updatedAt)
|
|
146
|
+
.where(sql`${table.type} = 'certificate'`),
|
|
147
|
+
certRenewalIdx: index('items_cert_renewal_idx')
|
|
148
|
+
.on(table.expiresAt)
|
|
149
|
+
.where(sql`${table.type} = 'certificate' AND ${table.status} = 'active'`),
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// =============================================================================
|
|
154
|
+
// BUILD ARTIFACTS (STI: index | ci_artifact | image | cache | ota | ota_asset)
|
|
155
|
+
// =============================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Unified build artifacts table
|
|
159
|
+
*
|
|
160
|
+
* OTA-specific types:
|
|
161
|
+
* type='ota' — OTA release manifests (Expo/EAS)
|
|
162
|
+
* s3Key=storagePrefix, runId=releaseRunId, platform, runtimeVersion,
|
|
163
|
+
* data: { updateId, expoConfig, metadata, isRollback }
|
|
164
|
+
*
|
|
165
|
+
* type='ota_asset' — Individual OTA assets (JS bundles, images)
|
|
166
|
+
* s3Key=storageKey, contentHash=hash, filename=key, mimeType=contentType,
|
|
167
|
+
* data: { fileExtension, isLaunchAsset }
|
|
168
|
+
*
|
|
169
|
+
* Uses partial composite index for EAS update lookups:
|
|
170
|
+
* build_artifacts_ota_lookup_idx (repositoryId, platform, runtimeVersion) WHERE type = 'ota'
|
|
171
|
+
*/
|
|
172
|
+
export const buildArtifacts = opsSchema.table(
|
|
173
|
+
'build_artifacts',
|
|
174
|
+
{
|
|
175
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
176
|
+
type: text('type').notNull().default('index'),
|
|
177
|
+
repositoryId: uuid('repository_id'),
|
|
178
|
+
userId: uuid('user_id'),
|
|
179
|
+
|
|
180
|
+
// Naming (shared)
|
|
181
|
+
name: text('name'),
|
|
182
|
+
path: text('path'),
|
|
183
|
+
filename: text('filename'),
|
|
184
|
+
tag: text('tag'),
|
|
185
|
+
|
|
186
|
+
// Content identity
|
|
187
|
+
contentHash: text('content_hash'),
|
|
188
|
+
digest: text('digest'),
|
|
189
|
+
sizeBytes: bigint('size_bytes', { mode: 'number' }),
|
|
190
|
+
|
|
191
|
+
// Build target
|
|
192
|
+
platform: text('platform'),
|
|
193
|
+
runtimeVersion: text('runtime_version'),
|
|
194
|
+
|
|
195
|
+
// Index-specific
|
|
196
|
+
contentText: text('content_text'),
|
|
197
|
+
lineCount: integer('line_count'),
|
|
198
|
+
language: text('language'),
|
|
199
|
+
extension: text('extension'),
|
|
200
|
+
buildDirectory: text('build_directory'),
|
|
201
|
+
buildType: text('build_type'),
|
|
202
|
+
|
|
203
|
+
// Storage
|
|
204
|
+
s3Key: text('s3_key'),
|
|
205
|
+
ecrUri: text('ecr_uri'),
|
|
206
|
+
mimeType: text('mime_type'),
|
|
207
|
+
|
|
208
|
+
// Cache-specific
|
|
209
|
+
cacheKey: text('cache_key'),
|
|
210
|
+
accessCount: integer('access_count').default(0),
|
|
211
|
+
|
|
212
|
+
// References
|
|
213
|
+
runId: uuid('run_id'),
|
|
214
|
+
|
|
215
|
+
// Lifecycle
|
|
216
|
+
fileModifiedAt: timestamp('file_modified_at', { withTimezone: true }),
|
|
217
|
+
lastAccessedAt: timestamp('last_accessed_at', { withTimezone: true }),
|
|
218
|
+
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
|
219
|
+
indexedAt: timestamp('indexed_at', { withTimezone: true }).defaultNow().notNull(),
|
|
220
|
+
|
|
221
|
+
// Type-specific overflow
|
|
222
|
+
data: jsonb('data'),
|
|
223
|
+
},
|
|
224
|
+
(table) => ({
|
|
225
|
+
typeIdx: index('build_artifacts_type_idx').on(table.type),
|
|
226
|
+
repoTypeIdx: index('build_artifacts_repo_type_idx').on(table.repositoryId, table.type),
|
|
227
|
+
repoPathIdx: uniqueIndex('build_artifacts_repo_path_idx').on(table.repositoryId, table.path),
|
|
228
|
+
buildDirIdx: index('build_artifacts_build_dir_idx').on(
|
|
229
|
+
table.repositoryId,
|
|
230
|
+
table.buildDirectory
|
|
231
|
+
),
|
|
232
|
+
extensionIdx: index('build_artifacts_extension_idx').on(table.extension),
|
|
233
|
+
languageIdx: index('build_artifacts_language_idx').on(table.language),
|
|
234
|
+
contentHashIdx: index('build_artifacts_content_hash_idx').on(table.contentHash),
|
|
235
|
+
filenameIdx: index('build_artifacts_filename_idx').on(table.filename),
|
|
236
|
+
cacheKeyIdx: index('build_artifacts_cache_key_idx').on(table.cacheKey),
|
|
237
|
+
runIdx: index('build_artifacts_run_idx').on(table.runId),
|
|
238
|
+
expiresIdx: index('build_artifacts_expires_idx').on(table.expiresAt),
|
|
239
|
+
platformIdx: index('build_artifacts_platform_idx').on(table.platform),
|
|
240
|
+
runtimeVersionIdx: index('build_artifacts_runtime_version_idx').on(table.runtimeVersion),
|
|
241
|
+
otaLookupIdx: index('build_artifacts_ota_lookup_idx')
|
|
242
|
+
.on(table.repositoryId, table.platform, table.runtimeVersion)
|
|
243
|
+
.where(sql`${table.type} = 'ota'`),
|
|
244
|
+
})
|
|
245
|
+
);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { StorageAdapter } from './index';
|
|
4
|
+
|
|
5
|
+
export class FilesystemStorageAdapter implements StorageAdapter {
|
|
6
|
+
private directory: string;
|
|
7
|
+
|
|
8
|
+
constructor(directory: string) {
|
|
9
|
+
this.directory = directory;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private resolvePath(key: string): string {
|
|
13
|
+
return path.join(this.directory, key);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async put(key: string, data: Buffer | Uint8Array, _contentType?: string): Promise<void> {
|
|
17
|
+
const filePath = this.resolvePath(key);
|
|
18
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
19
|
+
await fs.writeFile(filePath, data);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async get(key: string): Promise<{ data: Buffer; contentType: string } | null> {
|
|
23
|
+
const filePath = this.resolvePath(key);
|
|
24
|
+
try {
|
|
25
|
+
const data = await fs.readFile(filePath);
|
|
26
|
+
const ext = path.extname(key).slice(1);
|
|
27
|
+
const contentType = mimeFromExtension(ext);
|
|
28
|
+
return { data, contentType };
|
|
29
|
+
} catch (err: any) {
|
|
30
|
+
if (err.code === 'ENOENT') return null;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async exists(key: string): Promise<boolean> {
|
|
36
|
+
const meta = await this.head(key);
|
|
37
|
+
return meta !== null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async head(key: string): Promise<{ etag: string; size?: number } | null> {
|
|
41
|
+
const filePath = this.resolvePath(key);
|
|
42
|
+
try {
|
|
43
|
+
const stat = await fs.stat(filePath);
|
|
44
|
+
return {
|
|
45
|
+
etag: `${stat.size}-${stat.mtimeMs}`,
|
|
46
|
+
size: stat.size,
|
|
47
|
+
};
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
if (err.code === 'ENOENT') return null;
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async list(prefix: string): Promise<string[]> {
|
|
55
|
+
const dirPath = this.resolvePath(prefix);
|
|
56
|
+
try {
|
|
57
|
+
const entries = (await fs.readdir(dirPath, { recursive: true })) as string[];
|
|
58
|
+
const results: string[] = [];
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const fullKey = prefix ? `${prefix}/${entry}` : entry;
|
|
61
|
+
const fullPath = this.resolvePath(fullKey);
|
|
62
|
+
const stat = await fs.stat(fullPath);
|
|
63
|
+
if (stat.isFile()) {
|
|
64
|
+
results.push(fullKey);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return results.sort();
|
|
68
|
+
} catch (err: any) {
|
|
69
|
+
if (err.code === 'ENOENT') return [];
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async delete(key: string): Promise<void> {
|
|
75
|
+
const filePath = this.resolvePath(key);
|
|
76
|
+
try {
|
|
77
|
+
await fs.unlink(filePath);
|
|
78
|
+
} catch (err: any) {
|
|
79
|
+
if (err.code === 'ENOENT') return;
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mimeFromExtension(ext: string): string {
|
|
86
|
+
const mimeMap: Record<string, string> = {
|
|
87
|
+
js: 'application/javascript',
|
|
88
|
+
json: 'application/json',
|
|
89
|
+
png: 'image/png',
|
|
90
|
+
jpg: 'image/jpeg',
|
|
91
|
+
jpeg: 'image/jpeg',
|
|
92
|
+
gif: 'image/gif',
|
|
93
|
+
svg: 'image/svg+xml',
|
|
94
|
+
ttf: 'font/ttf',
|
|
95
|
+
otf: 'font/otf',
|
|
96
|
+
woff: 'font/woff',
|
|
97
|
+
woff2: 'font/woff2',
|
|
98
|
+
html: 'text/html',
|
|
99
|
+
css: 'text/css',
|
|
100
|
+
bundle: 'application/javascript',
|
|
101
|
+
};
|
|
102
|
+
return mimeMap[ext] || 'application/octet-stream';
|
|
103
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface StorageAdapter {
|
|
2
|
+
put(key: string, data: Buffer | Uint8Array, contentType?: string): Promise<void>;
|
|
3
|
+
get(key: string): Promise<{ data: Buffer; contentType: string } | null>;
|
|
4
|
+
exists(key: string): Promise<boolean>;
|
|
5
|
+
/** Returns object metadata (ETag, size), or null if not found. */
|
|
6
|
+
head(key: string): Promise<{ etag: string; size?: number } | null>;
|
|
7
|
+
list(prefix: string): Promise<string[]>;
|
|
8
|
+
delete(key: string): Promise<void>;
|
|
9
|
+
/** Generate a presigned PUT URL. S3-only — returns null for other backends. */
|
|
10
|
+
presignPut?(key: string, contentType: string, expiresIn?: number): Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type StorageOptions =
|
|
14
|
+
| { type: 'filesystem'; directory: string }
|
|
15
|
+
| { type: 's3'; bucket: string; region?: string; endpoint?: string };
|
|
16
|
+
|
|
17
|
+
export function createStorage(options: StorageOptions): StorageAdapter {
|
|
18
|
+
if (options.type === 'filesystem') {
|
|
19
|
+
const { FilesystemStorageAdapter } = require('./filesystem') as typeof import('./filesystem');
|
|
20
|
+
return new FilesystemStorageAdapter(options.directory);
|
|
21
|
+
}
|
|
22
|
+
const { S3StorageAdapter } = require('./s3') as typeof import('./s3');
|
|
23
|
+
return new S3StorageAdapter(options.bucket, options.region, options.endpoint);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { FilesystemStorageAdapter } from './filesystem';
|
|
27
|
+
export { S3StorageAdapter } from './s3';
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { StorageAdapter } from './index';
|
|
2
|
+
|
|
3
|
+
export class S3StorageAdapter implements StorageAdapter {
|
|
4
|
+
private bucket: string;
|
|
5
|
+
private region: string;
|
|
6
|
+
private endpoint?: string;
|
|
7
|
+
private client: any;
|
|
8
|
+
|
|
9
|
+
constructor(bucket: string, region?: string, endpoint?: string) {
|
|
10
|
+
this.bucket = bucket;
|
|
11
|
+
this.region = region || 'us-east-1';
|
|
12
|
+
this.endpoint = endpoint;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private async getClient(): Promise<any> {
|
|
16
|
+
if (!this.client) {
|
|
17
|
+
const { S3Client } = await import('@aws-sdk/client-s3');
|
|
18
|
+
this.client = new S3Client({
|
|
19
|
+
region: this.region,
|
|
20
|
+
...(this.endpoint ? { endpoint: this.endpoint, forcePathStyle: true } : {}),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return this.client;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async put(key: string, data: Buffer | Uint8Array, contentType?: string): Promise<void> {
|
|
27
|
+
const { PutObjectCommand } = await import('@aws-sdk/client-s3');
|
|
28
|
+
const client = await this.getClient();
|
|
29
|
+
await client.send(new PutObjectCommand({
|
|
30
|
+
Bucket: this.bucket,
|
|
31
|
+
Key: key,
|
|
32
|
+
Body: data,
|
|
33
|
+
ContentType: contentType || 'application/octet-stream',
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async get(key: string): Promise<{ data: Buffer; contentType: string } | null> {
|
|
38
|
+
const { GetObjectCommand } = await import('@aws-sdk/client-s3');
|
|
39
|
+
const client = await this.getClient();
|
|
40
|
+
try {
|
|
41
|
+
const response = await client.send(new GetObjectCommand({
|
|
42
|
+
Bucket: this.bucket,
|
|
43
|
+
Key: key,
|
|
44
|
+
}));
|
|
45
|
+
const bodyBytes = await response.Body!.transformToByteArray();
|
|
46
|
+
return {
|
|
47
|
+
data: Buffer.from(bodyBytes),
|
|
48
|
+
contentType: response.ContentType || 'application/octet-stream',
|
|
49
|
+
};
|
|
50
|
+
} catch (err: any) {
|
|
51
|
+
if (err.name === 'NoSuchKey' || err.$metadata?.httpStatusCode === 404) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async exists(key: string): Promise<boolean> {
|
|
59
|
+
const meta = await this.head(key);
|
|
60
|
+
return meta !== null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async head(key: string): Promise<{ etag: string; size?: number } | null> {
|
|
64
|
+
const { HeadObjectCommand } = await import('@aws-sdk/client-s3');
|
|
65
|
+
const client = await this.getClient();
|
|
66
|
+
try {
|
|
67
|
+
const response = await client.send(new HeadObjectCommand({
|
|
68
|
+
Bucket: this.bucket,
|
|
69
|
+
Key: key,
|
|
70
|
+
}));
|
|
71
|
+
return {
|
|
72
|
+
etag: response.ETag || '',
|
|
73
|
+
size: response.ContentLength,
|
|
74
|
+
};
|
|
75
|
+
} catch (err: any) {
|
|
76
|
+
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async list(prefix: string): Promise<string[]> {
|
|
84
|
+
const { ListObjectsV2Command } = await import('@aws-sdk/client-s3');
|
|
85
|
+
const client = await this.getClient();
|
|
86
|
+
const keys: string[] = [];
|
|
87
|
+
let continuationToken: string | undefined;
|
|
88
|
+
|
|
89
|
+
do {
|
|
90
|
+
const response = await client.send(new ListObjectsV2Command({
|
|
91
|
+
Bucket: this.bucket,
|
|
92
|
+
Prefix: prefix,
|
|
93
|
+
ContinuationToken: continuationToken,
|
|
94
|
+
}));
|
|
95
|
+
if (response.Contents) {
|
|
96
|
+
for (const obj of response.Contents) {
|
|
97
|
+
if (obj.Key) keys.push(obj.Key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
|
|
101
|
+
} while (continuationToken);
|
|
102
|
+
|
|
103
|
+
return keys;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async delete(key: string): Promise<void> {
|
|
107
|
+
const { DeleteObjectCommand } = await import('@aws-sdk/client-s3');
|
|
108
|
+
const client = await this.getClient();
|
|
109
|
+
await client.send(new DeleteObjectCommand({
|
|
110
|
+
Bucket: this.bucket,
|
|
111
|
+
Key: key,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async presignPut(key: string, contentType: string, expiresIn: number = 3600): Promise<string> {
|
|
116
|
+
const { PutObjectCommand } = await import('@aws-sdk/client-s3');
|
|
117
|
+
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
118
|
+
const client = await this.getClient();
|
|
119
|
+
return getSignedUrl(client, new PutObjectCommand({
|
|
120
|
+
Bucket: this.bucket,
|
|
121
|
+
Key: key,
|
|
122
|
+
ContentType: contentType,
|
|
123
|
+
}), { expiresIn });
|
|
124
|
+
}
|
|
125
|
+
}
|