fix: purge manage storage data when account delete (#176)

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-27 21:19:28 +08:00
committed by GitHub
parent b48f4bcd62
commit 843ff8130d
20 changed files with 218 additions and 21 deletions

View File

@@ -79,6 +79,12 @@ export interface StorageProvider {
*/
deleteFile: (key: string) => Promise<void>
/**
* 删除指定前缀下的所有文件(通常对应一个“目录”)
* @param prefix 需要删除的目录或前缀(不需要以 / 开头)
*/
deleteFolder: (prefix: string) => Promise<void>
/**
* 向存储上传文件
* @param key 文件的键值/路径

View File

@@ -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)

View File

@@ -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)

View File

@@ -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: 当前不支持上传文件操作')
}

View File

@@ -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, '')
}
}

View File

@@ -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)

View File

@@ -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, '')
}
}