mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
fix: purge manage storage data when account delete (#176)
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -79,6 +79,12 @@ export interface StorageProvider {
|
||||
*/
|
||||
deleteFile: (key: string) => Promise<void>
|
||||
|
||||
/**
|
||||
* 删除指定前缀下的所有文件(通常对应一个“目录”)
|
||||
* @param prefix 需要删除的目录或前缀(不需要以 / 开头)
|
||||
*/
|
||||
deleteFolder: (prefix: string) => Promise<void>
|
||||
|
||||
/**
|
||||
* 向存储上传文件
|
||||
* @param key 文件的键值/路径
|
||||
|
||||
@@ -110,6 +110,10 @@ export class StorageManager {
|
||||
await this.provider.deleteFile(key)
|
||||
}
|
||||
|
||||
async deleteFolder(prefix: string): Promise<void> {
|
||||
await this.provider.deleteFolder(prefix)
|
||||
}
|
||||
|
||||
async uploadFile(key: string, data: Buffer, options?: StorageUploadOptions): Promise<StorageObject> {
|
||||
const bytes = data?.byteLength ?? 0
|
||||
const progressHandler = this.createProgressPipeline(options?.onProgress)
|
||||
|
||||
@@ -562,6 +562,19 @@ export class B2StorageProvider implements StorageProvider {
|
||||
})
|
||||
}
|
||||
|
||||
async deleteFolder(prefix: string): Promise<void> {
|
||||
const normalizedPrefix = sanitizePath(prefix)
|
||||
const targetPrefix = normalizedPrefix ? `${normalizedPrefix}/` : ''
|
||||
const allFiles = await this.listAllFiles()
|
||||
const keysToDelete = allFiles
|
||||
.map((file) => file.key)
|
||||
.filter((key): key is string => Boolean(key) && (!targetPrefix || key.startsWith(targetPrefix)))
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
await this.deleteFile(key)
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(key: string, data: Buffer, options?: StorageUploadOptions): Promise<StorageObject> {
|
||||
const remoteKey = this.toRemoteKey(key)
|
||||
const file = await this.uploadInternal(remoteKey, data, options)
|
||||
|
||||
@@ -188,6 +188,10 @@ export class EagleStorageProvider implements StorageProvider {
|
||||
throw new Error('EagleStorageProvider: 当前不支持删除文件操作')
|
||||
}
|
||||
|
||||
async deleteFolder(_prefix: string): Promise<void> {
|
||||
throw new Error('EagleStorageProvider: 当前不支持删除目录操作')
|
||||
}
|
||||
|
||||
async uploadFile(_key: string, _data: Buffer, _options?: StorageUploadOptions): Promise<StorageObject> {
|
||||
throw new Error('EagleStorageProvider: 当前不支持上传文件操作')
|
||||
}
|
||||
|
||||
@@ -256,6 +256,19 @@ export class GitHubStorageProvider implements StorageProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFolder(prefix: string): Promise<void> {
|
||||
const normalizedPrefix = this.normalizePrefix(prefix)
|
||||
const targetPrefix = normalizedPrefix ? `${normalizedPrefix}/` : ''
|
||||
const allFiles = await this.listAllFiles()
|
||||
const keysToDelete = allFiles
|
||||
.map((file) => file.key)
|
||||
.filter((key): key is string => Boolean(key) && (!targetPrefix || key.startsWith(targetPrefix)))
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
await this.deleteFile(key)
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(key: string, data: Buffer, _options?: StorageUploadOptions): Promise<StorageObject> {
|
||||
const metadata = await this.fetchContentMetadata(key)
|
||||
const fullPath = this.getFullPath(key)
|
||||
@@ -385,4 +398,8 @@ export class GitHubStorageProvider implements StorageProvider {
|
||||
|
||||
return livePhotoMap
|
||||
}
|
||||
|
||||
private normalizePrefix(prefix: string): string {
|
||||
return prefix.replaceAll('\\', '/').replaceAll(/^\/+|\/+$/g, '')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,10 @@ export class LocalStorageProvider implements StorageProvider {
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
private normalizePrefix(prefix: string): string {
|
||||
return prefix.replaceAll('\\', '/').replaceAll(/^\/+|\/+$/g, '')
|
||||
}
|
||||
|
||||
private async syncDistFile(key: string, sourcePath: string): Promise<void> {
|
||||
if (!this.distPath) {
|
||||
return
|
||||
@@ -168,6 +172,19 @@ export class LocalStorageProvider implements StorageProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private async removeDistFolder(prefix: string): Promise<void> {
|
||||
if (!this.distPath) {
|
||||
return
|
||||
}
|
||||
|
||||
const targetPath = prefix ? path.join(this.distPath, prefix) : this.distPath
|
||||
try {
|
||||
await fs.rm(targetPath, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
this.logger.warn(`删除 dist 目录失败:${targetPath}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(key: string): Promise<void> {
|
||||
const filePath = this.resolveSafePath(key)
|
||||
|
||||
@@ -181,6 +198,20 @@ export class LocalStorageProvider implements StorageProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFolder(prefix: string): Promise<void> {
|
||||
const normalizedPrefix = this.normalizePrefix(prefix)
|
||||
const targetPath = normalizedPrefix ? this.resolveSafePath(normalizedPrefix) : this.basePath
|
||||
|
||||
try {
|
||||
await fs.rm(targetPath, { recursive: true, force: true })
|
||||
await this.removeDistFolder(normalizedPrefix)
|
||||
this.logger.success(`已删除本地目录:${normalizedPrefix || '.'}`)
|
||||
} catch (error) {
|
||||
this.logger.error(`删除本地目录失败:${normalizedPrefix || '.'}`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(key: string, data: Buffer, _options?: StorageUploadOptions): Promise<StorageObject> {
|
||||
const filePath = this.resolveSafePath(key)
|
||||
|
||||
|
||||
@@ -302,9 +302,9 @@ export class S3StorageProvider implements StorageProvider {
|
||||
return livePhotoMap
|
||||
}
|
||||
|
||||
private async listObjects(): Promise<StorageObject[]> {
|
||||
private async listObjects(prefix?: string): Promise<StorageObject[]> {
|
||||
const response = await this.client.listObjects({
|
||||
prefix: this.config.prefix,
|
||||
prefix: prefix ?? this.config.prefix,
|
||||
maxKeys: this.config.maxFileLimit,
|
||||
})
|
||||
const text = await response.text()
|
||||
@@ -337,6 +337,27 @@ export class S3StorageProvider implements StorageProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFolder(prefix: string): Promise<void> {
|
||||
const normalizedPrefix = this.normalizePrefix(prefix)
|
||||
const basePrefix = normalizedPrefix || this.config.prefix || ''
|
||||
const listPrefix = basePrefix || undefined
|
||||
const targetPrefix = basePrefix && !basePrefix.endsWith('/') ? `${basePrefix}/` : basePrefix
|
||||
|
||||
const objects = await this.listObjects(listPrefix)
|
||||
|
||||
const keysToDelete = objects
|
||||
.map((obj) => obj.key)
|
||||
.filter((key): key is string => {
|
||||
if (!key) return false
|
||||
if (!targetPrefix) return true
|
||||
return key.startsWith(targetPrefix)
|
||||
})
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
await this.deleteFile(key)
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(key: string, data: Buffer, options?: StorageUploadOptions): Promise<StorageObject> {
|
||||
const response = await this.client.putObject(key, data as unknown as BodyInit, {
|
||||
'content-type': options?.contentType ?? 'application/octet-stream',
|
||||
@@ -381,4 +402,8 @@ export class S3StorageProvider implements StorageProvider {
|
||||
etag: sanitizeS3Etag(metadata.etag),
|
||||
}
|
||||
}
|
||||
|
||||
private normalizePrefix(prefix: string): string {
|
||||
return prefix.replaceAll('\\', '/').replaceAll(/^\/+|\/+$/g, '')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user