Files
OpenFlare/frontend/lib/services/upload/upload.service.ts
T
ryan 5b13d5b464 feat(storage): implement user-group level file management dashboard and APIs
- Expose user-scoped CRUD APIs under `/api/v1/upload` (my files query, stats, rename, delete)
- Update backend handlers and routers with ownership validation checks
- Create a dedicated frontend personal file manager card-list and upload button under `/files`
- Add comprehensive backend test coverage and update API docs
2026-06-13 16:16:43 +08:00

190 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {BaseService} from '../core/base.service'
import type {ListUploadsResponse, Upload, UploadImageResponse, FileStatsResponse} from './types'
import type {InternalAxiosRequestConfig} from 'axios'
import apiClient from '../core/api-client'
export type ImageQuality = 'low' | 'medium' | 'high' | 'origin'
/**
* 根据上传ID构造文件访问URL
* @param id - 上传记录ID
* @param quality - 图片质量
* @returns 文件访问URL
*/
export function getFileUrl(
id: string | number | null | undefined,
quality: ImageQuality = 'origin'
): string | null {
if (!id) return null
if (quality === 'origin') return `/f/${id}`
return `/f/${id}?quality=${quality}`
}
/**
* 格式化文件大小
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
}
/**
* 上传服务
* 处理文件上传相关的 API 请求
*/
export class UploadService extends BaseService {
protected static readonly basePath = '/api/v1/admin/uploads'
/**
* 通用文件上传
* @param file - 文件对象
* @param type - 业务分类(如 avatar、attachment、generic)
* @param metadata - 可选额外 JSON 元数据
*/
static async uploadFile(
file: File,
type: string = 'generic',
metadata?: Record<string, unknown>,
accessMode?: number
): Promise<Upload> {
const formData = new FormData()
formData.append('file', file)
formData.append('type', type)
if (metadata) {
formData.append('metadata', JSON.stringify(metadata))
}
if (accessMode !== undefined) {
formData.append('access_mode', String(accessMode))
}
const response = await apiClient.post<{ data: Upload }>('/api/v1/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
} as InternalAxiosRequestConfig)
return response.data.data
}
/**
* 获取文件列表
* @param page - 页码(1-based)
* @param pageSize - 每页数量
* @param keyword - 搜索关键词(文件名模糊)
* @param type - 业务分类过滤
* @param extension - 扩展名过滤
*/
static async listUploads(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
extension?: string
): Promise<ListUploadsResponse> {
const params: Record<string, string | number> = { page, page_size: pageSize }
if (keyword) params.keyword = keyword
if (type) params.type = type
if (extension) params.extension = extension
return this.get<ListUploadsResponse>('', params)
}
/**
* 获取当前用户文件统计信息
*/
static async getFileStats(): Promise<FileStatsResponse> {
return this.get<FileStatsResponse>('/stats')
}
/**
* 删除文件
*/
static async deleteFile(id: string): Promise<void> {
return this.delete<void>(`/${id}`)
}
/**
* 获取单文件下载 URL(触发 attachment 下载)
*/
static getDownloadUrl(id: string): string {
return `/api/v1/admin/uploads/download/${id}`
}
/**
* 批量 ZIP 打包下载
* @param ids - 文件 ID 数组
*/
static async batchDownload(ids: string[]): Promise<Blob> {
const response = await this.post<Blob>('/download/batch', { ids }, {
responseType: 'blob',
} as InternalAxiosRequestConfig)
return response
}
/**
* 将 base64 图片转换为 Blob 并上传(兼容旧接口)
*/
static async uploadBase64Image(
base64: string,
type: string = 'generic',
filename: string = 'image.png',
accessMode?: number
): Promise<UploadImageResponse> {
const response = await fetch(base64)
const blob = await response.blob()
const mimeType = base64.match(/data:([^;]+);/)?.[1] || 'image/png'
const file = new File([blob], filename, { type: mimeType })
const result = await this.uploadFile(file, type, undefined, accessMode)
return { id: result.id }
}
/**
* 获取我的文件列表
*/
static async listMyUploads(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
extension?: string
): Promise<ListUploadsResponse> {
const params: Record<string, string | number> = { page, page_size: pageSize }
if (keyword) params.keyword = keyword
if (type) params.type = type
if (extension) params.extension = extension
const response = await apiClient.get<{ data: ListUploadsResponse }>('/api/v1/upload/my', {
params
} as InternalAxiosRequestConfig)
return response.data.data
}
/**
* 删除我的文件
*/
static async deleteMyFile(id: string): Promise<void> {
const response = await apiClient.delete<{ data: void }>(`/api/v1/upload/${id}`)
return response.data.data
}
/**
* 更新我的文件
*/
static async updateMyFile(id: string, fileName: string, accessMode?: number): Promise<Upload> {
const response = await apiClient.put<{ data: Upload }>(`/api/v1/upload/${id}`, {
file_name: fileName,
access_mode: accessMode
})
return response.data.data
}
/**
* 我的文件批量 ZIP 打包下载
*/
static async batchDownloadMyFiles(ids: string[]): Promise<Blob> {
const response = await apiClient.post<Blob>('/api/v1/upload/download/batch', { ids }, {
responseType: 'blob',
} as InternalAxiosRequestConfig)
return response.data
}
}