@nuasite/cms 0.42.1 → 0.43.0-beta.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.
- package/dist/editor.js +1 -1
- package/package.json +3 -1
- package/src/dev-middleware.ts +12 -1
- package/src/handlers/api-routes.ts +192 -48
- package/src/handlers/page-ops.ts +4 -189
- package/src/index.ts +9 -4
- package/src/media/types.ts +11 -55
- package/src/tsconfig.json +5 -1
- package/src/types.ts +31 -225
- package/src/handlers/markdown-ops.ts +0 -474
- package/src/handlers/redirect-ops.ts +0 -163
- package/src/media/contember.ts +0 -85
- package/src/media/local.ts +0 -152
- package/src/media/project-images.ts +0 -81
- package/src/media/s3.ts +0 -154
package/src/media/contember.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type { MediaListResult, MediaStorageAdapter, MediaUploadResult } from './types'
|
|
2
|
-
|
|
3
|
-
export interface ContemberStorageOptions {
|
|
4
|
-
/** Base URL of the worker API, e.g. 'https://api.example.com' */
|
|
5
|
-
apiBaseUrl: string
|
|
6
|
-
/** Project slug used in the API path */
|
|
7
|
-
projectSlug: string
|
|
8
|
-
/** Session token for authentication (NUA_SITE_SESSION_TOKEN cookie value) */
|
|
9
|
-
sessionToken?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Media storage adapter that proxies to the Contember worker's CMS media endpoints.
|
|
14
|
-
* Uses the existing /cms/:projectSlug/media/* API backed by R2 storage + Contember database.
|
|
15
|
-
*/
|
|
16
|
-
export function createContemberStorageAdapter(options: ContemberStorageOptions): MediaStorageAdapter {
|
|
17
|
-
const { apiBaseUrl, projectSlug, sessionToken } = options
|
|
18
|
-
const base = `${apiBaseUrl.replace(/\/$/, '')}/cms/${projectSlug}/media`
|
|
19
|
-
|
|
20
|
-
function headers(): Record<string, string> {
|
|
21
|
-
const h: Record<string, string> = {}
|
|
22
|
-
if (sessionToken) {
|
|
23
|
-
h.Cookie = `NUA_SITE_SESSION_TOKEN=${sessionToken}`
|
|
24
|
-
}
|
|
25
|
-
return h
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
async list(opts) {
|
|
30
|
-
const params = new URLSearchParams()
|
|
31
|
-
if (opts?.limit) params.set('limit', String(opts.limit))
|
|
32
|
-
if (opts?.cursor) params.set('cursor', opts.cursor)
|
|
33
|
-
|
|
34
|
-
const url = `${base}/list${params.toString() ? `?${params}` : ''}`
|
|
35
|
-
const res = await fetch(url, {
|
|
36
|
-
method: 'GET',
|
|
37
|
-
headers: headers(),
|
|
38
|
-
credentials: 'include',
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
if (!res.ok) {
|
|
42
|
-
throw new Error(`Failed to list media (${res.status}): ${await res.text()}`)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const data = await res.json()
|
|
46
|
-
return { folders: [], ...data } as MediaListResult
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
async upload(file, filename, contentType) {
|
|
50
|
-
// Build a FormData with the file buffer
|
|
51
|
-
const blob = new Blob([new Uint8Array(file)], { type: contentType })
|
|
52
|
-
const formData = new FormData()
|
|
53
|
-
formData.append('file', blob, filename)
|
|
54
|
-
|
|
55
|
-
const res = await fetch(`${base}/upload`, {
|
|
56
|
-
method: 'POST',
|
|
57
|
-
headers: headers(),
|
|
58
|
-
body: formData,
|
|
59
|
-
credentials: 'include',
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
if (!res.ok) {
|
|
63
|
-
const text = await res.text().catch(() => '')
|
|
64
|
-
return { success: false, error: `Upload failed (${res.status}): ${text}` }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return (await res.json()) as MediaUploadResult
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
async delete(id) {
|
|
71
|
-
const res = await fetch(`${base}/${encodeURIComponent(id)}`, {
|
|
72
|
-
method: 'DELETE',
|
|
73
|
-
headers: headers(),
|
|
74
|
-
credentials: 'include',
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
if (!res.ok) {
|
|
78
|
-
const text = await res.text().catch(() => '')
|
|
79
|
-
return { success: false, error: `Delete failed (${res.status}): ${text}` }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return { success: true }
|
|
83
|
-
},
|
|
84
|
-
}
|
|
85
|
-
}
|
package/src/media/local.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import fs from 'node:fs/promises'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import type { MediaFolderItem, MediaListResult, MediaStorageAdapter, MediaUploadResult } from './types'
|
|
5
|
-
|
|
6
|
-
export interface LocalStorageOptions {
|
|
7
|
-
/** Directory to store media files (relative to project root or absolute). Default: 'public/uploads' */
|
|
8
|
-
dir?: string
|
|
9
|
-
/** URL prefix for serving files. Default: '/uploads' */
|
|
10
|
-
urlPrefix?: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function createLocalStorageAdapter(options: LocalStorageOptions = {}): MediaStorageAdapter {
|
|
14
|
-
const dir = path.resolve(options.dir ?? 'public/uploads')
|
|
15
|
-
const urlPrefix = (options.urlPrefix ?? '/uploads').replace(/\/$/, '')
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
staticFiles: { urlPrefix, dir },
|
|
19
|
-
|
|
20
|
-
async list(opts) {
|
|
21
|
-
const limit = opts?.limit ?? 50
|
|
22
|
-
const offset = opts?.cursor ? parseInt(opts.cursor, 10) : 0
|
|
23
|
-
const folder = opts?.folder ?? ''
|
|
24
|
-
|
|
25
|
-
const targetDir = folder ? path.join(dir, folder) : dir
|
|
26
|
-
await fs.mkdir(targetDir, { recursive: true })
|
|
27
|
-
|
|
28
|
-
const entries = await fs.readdir(targetDir, { withFileTypes: true })
|
|
29
|
-
|
|
30
|
-
// Collect subfolders
|
|
31
|
-
const folders: MediaFolderItem[] = entries
|
|
32
|
-
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
33
|
-
.map((e) => ({
|
|
34
|
-
name: e.name,
|
|
35
|
-
path: folder ? `${folder}/${e.name}` : e.name,
|
|
36
|
-
}))
|
|
37
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
38
|
-
|
|
39
|
-
// Collect files
|
|
40
|
-
const files = entries.filter((e) => e.isFile() && !e.name.startsWith('.'))
|
|
41
|
-
|
|
42
|
-
// Get stats for sorting by mtime desc
|
|
43
|
-
const withStats = await Promise.all(
|
|
44
|
-
files.map(async (f) => {
|
|
45
|
-
const filePath = path.join(targetDir, f.name)
|
|
46
|
-
const stat = await fs.stat(filePath)
|
|
47
|
-
return { name: f.name, stat }
|
|
48
|
-
}),
|
|
49
|
-
)
|
|
50
|
-
withStats.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs)
|
|
51
|
-
|
|
52
|
-
const slice = withStats.slice(offset, offset + limit)
|
|
53
|
-
const hasMore = offset + limit < withStats.length
|
|
54
|
-
|
|
55
|
-
const urlFolder = folder ? `/${folder}` : ''
|
|
56
|
-
const items = slice.map((f) => {
|
|
57
|
-
const ext = path.extname(f.name).toLowerCase()
|
|
58
|
-
const contentType = mimeFromExt(ext)
|
|
59
|
-
return {
|
|
60
|
-
id: folder ? `${folder}/${f.name}` : f.name,
|
|
61
|
-
url: `${urlPrefix}${urlFolder}/${f.name}`,
|
|
62
|
-
filename: f.name,
|
|
63
|
-
contentType,
|
|
64
|
-
uploadedAt: f.stat.mtime.toISOString(),
|
|
65
|
-
folder: folder || undefined,
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
items,
|
|
71
|
-
folders,
|
|
72
|
-
hasMore,
|
|
73
|
-
cursor: hasMore ? String(offset + limit) : undefined,
|
|
74
|
-
} satisfies MediaListResult
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
async upload(file, filename, contentType, uploadOpts) {
|
|
78
|
-
const folder = uploadOpts?.folder ?? ''
|
|
79
|
-
const targetDir = folder ? path.join(dir, folder) : dir
|
|
80
|
-
await fs.mkdir(targetDir, { recursive: true })
|
|
81
|
-
|
|
82
|
-
const ext = getFileExtension(filename)
|
|
83
|
-
const uuid = randomUUID()
|
|
84
|
-
const newFilename = `${uuid}${ext ? `.${ext}` : ''}`
|
|
85
|
-
const filePath = path.join(targetDir, newFilename)
|
|
86
|
-
|
|
87
|
-
await fs.writeFile(filePath, file)
|
|
88
|
-
|
|
89
|
-
const urlFolder = folder ? `/${folder}` : ''
|
|
90
|
-
const id = folder ? `${folder}/${newFilename}` : newFilename
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
success: true,
|
|
94
|
-
url: `${urlPrefix}${urlFolder}/${newFilename}`,
|
|
95
|
-
filename: newFilename,
|
|
96
|
-
id,
|
|
97
|
-
} satisfies MediaUploadResult
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
async delete(id) {
|
|
101
|
-
// id may contain folder path — resolve safely within dir
|
|
102
|
-
const safePath = id.split('/').map((s) => path.basename(s)).join('/')
|
|
103
|
-
const filePath = path.join(dir, safePath)
|
|
104
|
-
try {
|
|
105
|
-
await fs.unlink(filePath)
|
|
106
|
-
return { success: true }
|
|
107
|
-
} catch (error) {
|
|
108
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
109
|
-
return { success: false, error: message }
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
async createFolder(folder) {
|
|
114
|
-
const segments = folder.split('/').filter(Boolean)
|
|
115
|
-
if (segments.some((s) => s === '..' || s === '.')) {
|
|
116
|
-
return { success: false, error: 'Invalid folder name' }
|
|
117
|
-
}
|
|
118
|
-
try {
|
|
119
|
-
await fs.mkdir(path.join(dir, ...segments), { recursive: true })
|
|
120
|
-
return { success: true }
|
|
121
|
-
} catch (error) {
|
|
122
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
123
|
-
return { success: false, error: message }
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function getFileExtension(filename: string): string {
|
|
130
|
-
const parts = filename.split('.')
|
|
131
|
-
const ext = parts.length > 1 ? (parts.pop()?.toLowerCase() ?? '') : ''
|
|
132
|
-
// Only allow alphanumeric extensions to prevent injection
|
|
133
|
-
return /^[a-z0-9]+$/.test(ext) ? ext : ''
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export const MIME_BY_EXT: Record<string, string> = {
|
|
137
|
-
'.jpg': 'image/jpeg',
|
|
138
|
-
'.jpeg': 'image/jpeg',
|
|
139
|
-
'.png': 'image/png',
|
|
140
|
-
'.gif': 'image/gif',
|
|
141
|
-
'.webp': 'image/webp',
|
|
142
|
-
'.avif': 'image/avif',
|
|
143
|
-
'.svg': 'image/svg+xml',
|
|
144
|
-
'.ico': 'image/x-icon',
|
|
145
|
-
'.mp4': 'video/mp4',
|
|
146
|
-
'.webm': 'video/webm',
|
|
147
|
-
'.pdf': 'application/pdf',
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function mimeFromExt(ext: string): string {
|
|
151
|
-
return MIME_BY_EXT[ext] ?? 'application/octet-stream'
|
|
152
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { Dirent } from 'node:fs'
|
|
2
|
-
import fs from 'node:fs/promises'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { getProjectRoot } from '../config'
|
|
5
|
-
import { MIME_BY_EXT, mimeFromExt } from './local'
|
|
6
|
-
import type { MediaItem } from './types'
|
|
7
|
-
|
|
8
|
-
const IMAGE_EXTENSIONS = new Set(Object.entries(MIME_BY_EXT).filter(([, mime]) => mime.startsWith('image/')).map(([ext]) => ext))
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Scan the project for image files in `public/` and `src/` directories,
|
|
12
|
-
* excluding the media uploads directory to avoid duplicates.
|
|
13
|
-
*/
|
|
14
|
-
export async function listProjectImages(options?: {
|
|
15
|
-
excludeDir?: string
|
|
16
|
-
}): Promise<MediaItem[]> {
|
|
17
|
-
const root = getProjectRoot()
|
|
18
|
-
const excludeDir = options?.excludeDir ? path.resolve(options.excludeDir) : null
|
|
19
|
-
|
|
20
|
-
const scanDirs = [
|
|
21
|
-
{ dir: path.join(root, 'public'), urlPrefix: '' },
|
|
22
|
-
{ dir: path.join(root, 'src'), urlPrefix: null },
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
const results = await Promise.all(
|
|
26
|
-
scanDirs.map(({ dir, urlPrefix }) => {
|
|
27
|
-
const items: MediaItem[] = []
|
|
28
|
-
return scanDirectory(dir, dir, urlPrefix, excludeDir, items).then(() => items)
|
|
29
|
-
}),
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
const items = results.flat()
|
|
33
|
-
items.sort((a, b) => a.filename.localeCompare(b.filename))
|
|
34
|
-
return items
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function scanDirectory(
|
|
38
|
-
currentDir: string,
|
|
39
|
-
baseDir: string,
|
|
40
|
-
urlPrefix: string | null,
|
|
41
|
-
excludeDir: string | null,
|
|
42
|
-
items: MediaItem[],
|
|
43
|
-
): Promise<void> {
|
|
44
|
-
if (excludeDir && path.resolve(currentDir) === excludeDir) return
|
|
45
|
-
|
|
46
|
-
let entries: Dirent[]
|
|
47
|
-
try {
|
|
48
|
-
entries = await fs.readdir(currentDir, { withFileTypes: true }) as Dirent[]
|
|
49
|
-
} catch {
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const subdirs: Promise<void>[] = []
|
|
54
|
-
|
|
55
|
-
for (const entry of entries) {
|
|
56
|
-
const name = String(entry.name)
|
|
57
|
-
if (name.startsWith('.') || name === 'node_modules') continue
|
|
58
|
-
const fullPath = path.join(currentDir, name)
|
|
59
|
-
|
|
60
|
-
if (entry.isDirectory()) {
|
|
61
|
-
subdirs.push(scanDirectory(fullPath, baseDir, urlPrefix, excludeDir, items))
|
|
62
|
-
} else if (entry.isFile()) {
|
|
63
|
-
const ext = path.extname(name).toLowerCase()
|
|
64
|
-
if (!IMAGE_EXTENSIONS.has(ext)) continue
|
|
65
|
-
|
|
66
|
-
const relativePath = path.relative(baseDir, fullPath).split(path.sep).join('/')
|
|
67
|
-
const url = urlPrefix !== null
|
|
68
|
-
? `/${relativePath}`
|
|
69
|
-
: `/${path.relative(getProjectRoot(), fullPath).split(path.sep).join('/')}`
|
|
70
|
-
|
|
71
|
-
items.push({
|
|
72
|
-
id: `project:${url}`,
|
|
73
|
-
url,
|
|
74
|
-
filename: name,
|
|
75
|
-
contentType: mimeFromExt(ext),
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
await Promise.all(subdirs)
|
|
81
|
-
}
|
package/src/media/s3.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { getFileExtension, mimeFromExt } from './local'
|
|
4
|
-
import type { MediaFolderItem, MediaListResult, MediaStorageAdapter, MediaUploadResult } from './types'
|
|
5
|
-
|
|
6
|
-
export interface S3StorageOptions {
|
|
7
|
-
bucket: string
|
|
8
|
-
region: string
|
|
9
|
-
accessKeyId?: string
|
|
10
|
-
secretAccessKey?: string
|
|
11
|
-
endpoint?: string
|
|
12
|
-
cdnPrefix?: string
|
|
13
|
-
prefix?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Dynamic import helper to avoid TS2307 for optional peer dependency
|
|
17
|
-
const s3Module = '@aws-sdk/client-s3'
|
|
18
|
-
async function loadS3(): Promise<any> {
|
|
19
|
-
return import(/* @vite-ignore */ s3Module)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function createS3StorageAdapter(options: S3StorageOptions): MediaStorageAdapter {
|
|
23
|
-
const { bucket, region, accessKeyId, secretAccessKey, endpoint, cdnPrefix, prefix = 'uploads' } = options
|
|
24
|
-
|
|
25
|
-
let s3Client: any = null
|
|
26
|
-
|
|
27
|
-
async function getClient() {
|
|
28
|
-
if (s3Client) return s3Client
|
|
29
|
-
const { S3Client } = await loadS3()
|
|
30
|
-
s3Client = new S3Client({
|
|
31
|
-
region,
|
|
32
|
-
...(endpoint ? { endpoint } : {}),
|
|
33
|
-
...(accessKeyId && secretAccessKey
|
|
34
|
-
? { credentials: { accessKeyId, secretAccessKey } }
|
|
35
|
-
: {}),
|
|
36
|
-
})
|
|
37
|
-
return s3Client
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function getUrl(key: string): string {
|
|
41
|
-
if (cdnPrefix) {
|
|
42
|
-
return `${cdnPrefix.replace(/\/$/, '')}/${key}`
|
|
43
|
-
}
|
|
44
|
-
if (endpoint) {
|
|
45
|
-
return `${endpoint.replace(/\/$/, '')}/${bucket}/${key}`
|
|
46
|
-
}
|
|
47
|
-
return `https://${bucket}.s3.${region}.amazonaws.com/${key}`
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
async list(opts) {
|
|
52
|
-
const { ListObjectsV2Command } = await loadS3()
|
|
53
|
-
const client = await getClient()
|
|
54
|
-
|
|
55
|
-
const limit = opts?.limit ?? 50
|
|
56
|
-
const folder = opts?.folder ?? ''
|
|
57
|
-
|
|
58
|
-
// Build the S3 prefix: base prefix + subfolder
|
|
59
|
-
const listPrefix = [prefix, folder].filter(Boolean).join('/')
|
|
60
|
-
const delimiterPrefix = listPrefix ? `${listPrefix}/` : ''
|
|
61
|
-
|
|
62
|
-
const command = new ListObjectsV2Command({
|
|
63
|
-
Bucket: bucket,
|
|
64
|
-
Prefix: delimiterPrefix,
|
|
65
|
-
Delimiter: '/',
|
|
66
|
-
MaxKeys: limit + 1,
|
|
67
|
-
...(opts?.cursor ? { ContinuationToken: opts.cursor } : {}),
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const result = await client.send(command)
|
|
71
|
-
|
|
72
|
-
// Extract subfolders from CommonPrefixes
|
|
73
|
-
const folders: MediaFolderItem[] = (result.CommonPrefixes ?? []).map((cp: any) => {
|
|
74
|
-
const fullPrefix = (cp.Prefix as string).replace(/\/$/, '')
|
|
75
|
-
const name = fullPrefix.split('/').pop() ?? fullPrefix
|
|
76
|
-
const relativePath = prefix ? fullPrefix.slice(prefix.length + 1) : fullPrefix
|
|
77
|
-
return { name, path: relativePath }
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
// Extract files (exclude folder marker keys)
|
|
81
|
-
const contents = (result.Contents ?? []).filter((obj: any) => {
|
|
82
|
-
const key = obj.Key as string
|
|
83
|
-
return key !== delimiterPrefix
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
const hasMore = contents.length > limit
|
|
87
|
-
const items = contents.slice(0, limit).map((obj: any) => {
|
|
88
|
-
const key = obj.Key as string
|
|
89
|
-
const filename = key.split('/').pop() ?? key
|
|
90
|
-
return {
|
|
91
|
-
id: key,
|
|
92
|
-
url: getUrl(key),
|
|
93
|
-
filename,
|
|
94
|
-
contentType: mimeFromExt(path.extname(key).toLowerCase()),
|
|
95
|
-
uploadedAt: obj.LastModified?.toISOString(),
|
|
96
|
-
folder: folder || undefined,
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
items,
|
|
102
|
-
folders,
|
|
103
|
-
hasMore,
|
|
104
|
-
cursor: hasMore ? result.NextContinuationToken : undefined,
|
|
105
|
-
} satisfies MediaListResult
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
async upload(file, filename, contentType, uploadOpts) {
|
|
109
|
-
const { PutObjectCommand } = await loadS3()
|
|
110
|
-
const client = await getClient()
|
|
111
|
-
|
|
112
|
-
const ext = getFileExtension(filename)
|
|
113
|
-
const uuid = randomUUID()
|
|
114
|
-
const newFilename = `${uuid}${ext ? `.${ext}` : ''}`
|
|
115
|
-
const folder = uploadOpts?.folder ?? ''
|
|
116
|
-
const keyParts = [prefix, folder, newFilename].filter(Boolean)
|
|
117
|
-
const key = keyParts.join('/')
|
|
118
|
-
|
|
119
|
-
const command = new PutObjectCommand({
|
|
120
|
-
Bucket: bucket,
|
|
121
|
-
Key: key,
|
|
122
|
-
Body: file,
|
|
123
|
-
ContentType: contentType,
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
await client.send(command)
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
success: true,
|
|
130
|
-
url: getUrl(key),
|
|
131
|
-
filename: newFilename,
|
|
132
|
-
id: key,
|
|
133
|
-
} satisfies MediaUploadResult
|
|
134
|
-
},
|
|
135
|
-
|
|
136
|
-
async delete(id) {
|
|
137
|
-
try {
|
|
138
|
-
const { DeleteObjectCommand } = await loadS3()
|
|
139
|
-
const client = await getClient()
|
|
140
|
-
|
|
141
|
-
const command = new DeleteObjectCommand({
|
|
142
|
-
Bucket: bucket,
|
|
143
|
-
Key: id,
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
await client.send(command)
|
|
147
|
-
return { success: true }
|
|
148
|
-
} catch (error) {
|
|
149
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
150
|
-
return { success: false, error: message }
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
}
|
|
154
|
-
}
|