mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-07-01 04:01:30 +08:00
Initial commit
This commit is contained in:
34
web/src/api/adapters.ts
Normal file
34
web/src/api/adapters.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import request from './client';
|
||||
|
||||
export interface AdapterItem {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
config: any;
|
||||
enabled: boolean;
|
||||
mount_path?: string | null;
|
||||
sub_path?: string | null;
|
||||
}
|
||||
|
||||
export interface AdapterTypeField {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'string' | 'password' | 'number';
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
default?: any;
|
||||
}
|
||||
|
||||
export interface AdapterTypeMeta {
|
||||
type: string;
|
||||
name: string;
|
||||
config_schema: AdapterTypeField[];
|
||||
}
|
||||
|
||||
export const adaptersApi = {
|
||||
list: () => request<AdapterItem[]>('/adapters'),
|
||||
create: (payload: Omit<AdapterItem, 'id'>) => request<AdapterItem>('/adapters', { method: 'POST', json: payload }),
|
||||
update: (id: number, payload: Omit<AdapterItem, 'id'>) => request<AdapterItem>(`/adapters/${id}`, { method: 'PUT', json: payload }),
|
||||
remove: (id: number) => request<void>(`/adapters/${id}`, { method: 'DELETE' }),
|
||||
available: () => request<AdapterTypeMeta[]>('/adapters/available'),
|
||||
};
|
||||
45
web/src/api/auth.ts
Normal file
45
web/src/api/auth.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import request from './client';
|
||||
|
||||
export interface LoginPayload {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterPayload {
|
||||
username: string;
|
||||
password: string;
|
||||
email?: string;
|
||||
full_name?: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
register: async (username: string, password: string, email?: string, full_name?: string): Promise<any> => {
|
||||
return request('/auth/register', {
|
||||
method: 'POST',
|
||||
json: { username, password, email, full_name },
|
||||
});
|
||||
},
|
||||
login: async (payload: LoginPayload) => {
|
||||
const form = new URLSearchParams();
|
||||
form.append('username', payload.username);
|
||||
form.append('password', payload.password);
|
||||
try {
|
||||
return await request<AuthResponse>('/auth/login', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[authApi.login] error:', e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
logout: () => {
|
||||
localStorage.removeItem('token');
|
||||
},
|
||||
};
|
||||
38
web/src/api/backup.ts
Normal file
38
web/src/api/backup.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import request from './client';
|
||||
|
||||
export const backupApi = {
|
||||
export: async () => {
|
||||
const response = await request('/backup/export', {
|
||||
method: 'GET',
|
||||
rawResponse: true,
|
||||
}) as Response;
|
||||
|
||||
const contentDisposition = response.headers.get('content-disposition');
|
||||
let filename = 'backup.json';
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
|
||||
if (filenameMatch && filenameMatch.length > 1) {
|
||||
filename = filenameMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
|
||||
import: async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return request('/backup/import', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
},
|
||||
};
|
||||
76
web/src/api/client.ts
Normal file
76
web/src/api/client.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export interface RequestOptions extends RequestInit {
|
||||
json?: any;
|
||||
formData?: FormData;
|
||||
text?: string;
|
||||
rawResponse?: boolean;
|
||||
}
|
||||
const BASE_URL = import.meta.env.PROD ? '/api' : 'http://127.0.0.1:8000/api';
|
||||
export const API_BASE_URL = BASE_URL;
|
||||
|
||||
async function request<T = any>(url: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { json, formData, text, rawResponse, headers, ...rest } = options;
|
||||
const finalHeaders: Record<string, string> = {
|
||||
...(headers as Record<string, string> || {})
|
||||
};
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
finalHeaders['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
let body: BodyInit | undefined;
|
||||
if (json !== undefined) {
|
||||
body = JSON.stringify(json);
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
} else if (formData) {
|
||||
body = formData;
|
||||
} else if (text !== undefined) {
|
||||
body = text;
|
||||
finalHeaders['Content-Type'] = 'text/plain;charset=utf-8';
|
||||
} else if ((rest as any).body !== undefined) {
|
||||
body = (rest as any).body;
|
||||
delete (rest as any).body;
|
||||
}
|
||||
|
||||
const resp = await fetch(BASE_URL + url, {
|
||||
...rest,
|
||||
headers: finalHeaders,
|
||||
body,
|
||||
});
|
||||
|
||||
if (rawResponse) return resp as any;
|
||||
|
||||
if (!resp.ok) {
|
||||
let errMsg = resp.statusText;
|
||||
try {
|
||||
const data = await resp.json();
|
||||
if (Array.isArray(data?.detail)) {
|
||||
errMsg = data.detail.map((e: any) => e.msg || JSON.stringify(e)).join('; ');
|
||||
} else {
|
||||
errMsg = (typeof data?.detail === 'string') ? data.detail : (data.detail ? JSON.stringify(data.detail) : JSON.stringify(data));
|
||||
}
|
||||
} catch (_) { }
|
||||
throw new Error(errMsg || `Request failed: ${resp.status}`);
|
||||
}
|
||||
|
||||
const contentType = resp.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
const json = await resp.json();
|
||||
if (json && typeof json === 'object' && 'code' in json && ('msg' in json || 'message' in json)) {
|
||||
if (json.code !== 0) {
|
||||
throw new Error(json.msg || json.message || 'Error');
|
||||
}
|
||||
return json.data as T;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
if (contentType.startsWith('text/')) {
|
||||
return await resp.text() as any;
|
||||
}
|
||||
return await resp.arrayBuffer() as any;
|
||||
}
|
||||
|
||||
export { vfsApi, type VfsEntry, type DirListing } from './vfs';
|
||||
export { adaptersApi, type AdapterItem, type AdapterTypeField, type AdapterTypeMeta } from './adapters';
|
||||
export { shareApi } from './share';
|
||||
export default request;
|
||||
27
web/src/api/config.ts
Normal file
27
web/src/api/config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import request from './client';
|
||||
|
||||
export async function getConfig(key: string) {
|
||||
return request<{ key: string; value: string }>('/config?key=' + encodeURIComponent(key));
|
||||
}
|
||||
|
||||
export async function setConfig(key: string, value: string) {
|
||||
const form = new FormData();
|
||||
form.append('key', key);
|
||||
form.append('value', value);
|
||||
return request('/config', { method: 'POST', formData: form });
|
||||
}
|
||||
|
||||
export async function getAllConfig() {
|
||||
return request<Record<string, string>>('/config/all');
|
||||
}
|
||||
|
||||
export interface SystemStatus {
|
||||
version: string;
|
||||
title: string;
|
||||
logo: string;
|
||||
is_initialized: boolean;
|
||||
}
|
||||
|
||||
export async function status() {
|
||||
return request<SystemStatus>('/config/status');
|
||||
}
|
||||
54
web/src/api/logs.ts
Normal file
54
web/src/api/logs.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import request from './client';
|
||||
|
||||
export interface LogItem {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
level: string;
|
||||
source: string;
|
||||
message: string;
|
||||
details: Record<string, any>;
|
||||
user_id?: number;
|
||||
}
|
||||
|
||||
export interface PaginatedLogs {
|
||||
items: LogItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface GetLogsParams {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
level?: string;
|
||||
source?: string;
|
||||
start_time?: string;
|
||||
end_time?: string;
|
||||
}
|
||||
|
||||
export interface ClearLogsParams {
|
||||
start_time?: string;
|
||||
end_time?: string;
|
||||
}
|
||||
|
||||
export const logsApi = {
|
||||
list: (params: GetLogsParams = {}) => {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.append('page', params.page.toString());
|
||||
if (params.page_size) query.append('page_size', params.page_size.toString());
|
||||
if (params.level) query.append('level', params.level);
|
||||
if (params.source) query.append('source', params.source);
|
||||
if (params.start_time) query.append('start_time', params.start_time);
|
||||
if (params.end_time) query.append('end_time', params.end_time);
|
||||
return request<PaginatedLogs>(`/logs?${query.toString()}`);
|
||||
},
|
||||
clear: (params: ClearLogsParams = {}) => {
|
||||
const query = new URLSearchParams();
|
||||
if (params.start_time) query.append('start_time', params.start_time);
|
||||
if (params.end_time) query.append('end_time', params.end_time);
|
||||
return request<{ deleted_count: number }>(`/logs?${query.toString()}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
};
|
||||
39
web/src/api/processors.ts
Normal file
39
web/src/api/processors.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import request from './client';
|
||||
|
||||
export interface ProcessorTypeField {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'string' | 'password' | 'number' | 'select';
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
default?: any;
|
||||
options?: { label: string; value: string | number }[];
|
||||
}
|
||||
|
||||
export interface ProcessorTypeMeta {
|
||||
type: string;
|
||||
name: string;
|
||||
supported_exts: string[];
|
||||
config_schema: ProcessorTypeField[];
|
||||
produces_file:boolean;
|
||||
}
|
||||
|
||||
export const processorsApi = {
|
||||
list: () => request<ProcessorTypeMeta[]>('/processors', {
|
||||
method: 'GET'
|
||||
}),
|
||||
process: (params: {
|
||||
path: string;
|
||||
processor_type: string;
|
||||
config: any;
|
||||
save_to?: string;
|
||||
overwrite?: boolean;
|
||||
}) =>
|
||||
request<any>('/processors/process', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}),
|
||||
};
|
||||
39
web/src/api/share.ts
Normal file
39
web/src/api/share.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import request, { API_BASE_URL } from './client';
|
||||
import type { DirListing } from './vfs';
|
||||
|
||||
export interface ShareInfo {
|
||||
id: number;
|
||||
token: string;
|
||||
name: string;
|
||||
paths: string[];
|
||||
created_at: string;
|
||||
expires_at?: string;
|
||||
access_type: 'public' | 'password';
|
||||
}
|
||||
|
||||
export interface ShareCreatePayload {
|
||||
name: string;
|
||||
paths: string[];
|
||||
expires_in_days?: number;
|
||||
access_type: 'public' | 'password';
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export const shareApi = {
|
||||
create: (payload: ShareCreatePayload) => request<ShareInfo>('/shares', { method: 'POST', json: payload }),
|
||||
list: () => request<ShareInfo[]>('/shares'),
|
||||
remove: (shareId: number) => request<void>(`/shares/${shareId}`, { method: 'DELETE' }),
|
||||
get: (token: string) => request<ShareInfo>(`/s/${token}`),
|
||||
verifyPassword: (token: string, password: string) => request<void>(`/s/${token}/verify`, { method: 'POST', json: { password } }),
|
||||
listDir: (token: string, path: string = '/', password?: string) => {
|
||||
const params: Record<string, string> = { path };
|
||||
if (password) {
|
||||
params.password = password;
|
||||
}
|
||||
return request<DirListing>(`/s/${token}/ls?${new URLSearchParams(params)}`);
|
||||
},
|
||||
downloadUrl: (token: string, path: string, password?: string) => {
|
||||
const url = `${API_BASE_URL}/s/${token}/download?path=${encodeURIComponent(path)}`;
|
||||
return password ? `${url}&password=${encodeURIComponent(password)}` : url;
|
||||
},
|
||||
};
|
||||
22
web/src/api/tasks.ts
Normal file
22
web/src/api/tasks.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import request from './client';
|
||||
|
||||
export interface AutomationTask {
|
||||
id: number;
|
||||
name: string;
|
||||
event: string;
|
||||
path_pattern?: string;
|
||||
filename_regex?: string;
|
||||
processor_type: string;
|
||||
processor_config: Record<string, any>;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export type AutomationTaskCreate = Omit<AutomationTask, 'id'>;
|
||||
export type AutomationTaskUpdate = Partial<AutomationTaskCreate>;
|
||||
|
||||
export const tasksApi = {
|
||||
list: () => request<AutomationTask[]>('/tasks/'),
|
||||
create: (payload: AutomationTaskCreate) => request<AutomationTask>('/tasks', { method: 'POST', json: payload }),
|
||||
update: (id: number, payload: AutomationTaskUpdate) => request<AutomationTask>(`/tasks/${id}`, { method: 'PUT', json: payload }),
|
||||
remove: (id: number) => request<void>(`/tasks/${id}`, { method: 'DELETE' }),
|
||||
};
|
||||
92
web/src/api/vfs.ts
Normal file
92
web/src/api/vfs.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import request, { API_BASE_URL } from './client';
|
||||
|
||||
export interface VfsEntry {
|
||||
name: string;
|
||||
is_dir: boolean;
|
||||
size: number;
|
||||
mtime: number;
|
||||
type?: string;
|
||||
is_image?: boolean;
|
||||
}
|
||||
|
||||
export interface DirListing {
|
||||
path: string;
|
||||
entries: VfsEntry[];
|
||||
pagination?: {
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SearchResultItem {
|
||||
id: number;
|
||||
path: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export const vfsApi = {
|
||||
list: (path: string, page: number = 1, pageSize: number = 50) => {
|
||||
const cleaned = path.replace(/\\/g, '/');
|
||||
const trimmed = cleaned === '/' ? '' : cleaned.replace(/^\/+/, '');
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString()
|
||||
});
|
||||
return request<DirListing>(`/fs/${encodeURI(trimmed)}?${params}`);
|
||||
},
|
||||
readFile: (path: string) => request<ArrayBuffer>(`/fs/file/${encodeURI(path.replace(/^\/+/, ''))}`),
|
||||
uploadFile: (fullPath: string, file: File | Blob) => {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
return request(`/fs/file/${encodeURI(fullPath.replace(/^\/+/, ''))}`, { method: 'POST', formData: fd });
|
||||
},
|
||||
mkdir: (path: string) => request('/fs/mkdir', { method: 'POST', json: { path } }),
|
||||
deletePath: (path: string) => request(`/fs/${encodeURI(path.replace(/^\/+/, ''))}`, { method: 'DELETE' }),
|
||||
move: (src: string, dst: string) => request('/fs/move', { method: 'POST', json: { src, dst } }),
|
||||
rename: (src: string, dst: string) => request('/fs/rename', { method: 'POST', json: { src, dst } }),
|
||||
thumb: (path: string, w=256, h=256, fit='cover') =>
|
||||
request<ArrayBuffer>(`/fs/thumb/${encodeURI(path.replace(/^\/+/, ''))}?w=${w}&h=${h}&fit=${fit}`),
|
||||
streamUrl: (path: string) => `${API_BASE_URL}/fs/stream/${encodeURI(path.replace(/^\/+/, ''))}`,
|
||||
stat: (path: string) => request(`/fs/stat/${encodeURI(path.replace(/^\/+/, ''))}`),
|
||||
getTempLinkToken: (path: string) => request<{token: string}>(`/fs/temp-link/${encodeURI(path.replace(/^\/+/, ''))}`),
|
||||
getTempPublicUrl: (token: string) => `${API_BASE_URL}/fs/public/${token}`,
|
||||
uploadStream: (fullPath: string, file: File, overwrite: boolean = true, onProgress?: (loaded: number, total: number) => void) => {
|
||||
const enc = encodeURI(fullPath.replace(/^\/+/, ''));
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', `${API_BASE_URL}/fs/upload/${enc}?overwrite=${overwrite}`);
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||
xhr.upload.onprogress = (ev) => {
|
||||
if (ev.lengthComputable && onProgress) onProgress(ev.loaded, ev.total);
|
||||
};
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
const json = JSON.parse(xhr.responseText);
|
||||
if (json.code === 0) return resolve(json.data);
|
||||
return reject(new Error(json.msg || json.message || 'Upload failed'));
|
||||
} catch (e) {
|
||||
return reject(new Error('Invalid response'));
|
||||
}
|
||||
} else {
|
||||
let err = 'Upload failed';
|
||||
try {
|
||||
const json = JSON.parse(xhr.responseText);
|
||||
err = json.detail || json.msg || json.message || err;
|
||||
} catch (_) {}
|
||||
reject(new Error(err));
|
||||
}
|
||||
}
|
||||
};
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
xhr.send(fd);
|
||||
});
|
||||
},
|
||||
searchFiles: (q: string, top_k: number = 10, mode: 'vector' | 'filename' = 'vector') =>
|
||||
request<{ items: SearchResultItem[]; query: string }>(`/search?q=${encodeURIComponent(q)}&top_k=${top_k}&mode=${mode}`),
|
||||
};
|
||||
Reference in New Issue
Block a user