mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
fix(export): harden downloads and CSV encoding
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
collectUsageDetails,
|
||||
extractTotalTokens
|
||||
} from '@/utils/usage';
|
||||
import { downloadBlob } from '@/utils/download';
|
||||
import styles from '@/pages/UsagePage.module.scss';
|
||||
|
||||
const ALL_FILTER = '__all__';
|
||||
@@ -53,17 +54,9 @@ const toNumber = (value: unknown): number => {
|
||||
|
||||
const encodeCsv = (value: string | number): string => {
|
||||
const text = String(value ?? '');
|
||||
return `"${text.replace(/"/g, '""')}"`;
|
||||
};
|
||||
|
||||
const downloadFile = (filename: string, content: string, mimeType: string) => {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
const trimmedLeft = text.replace(/^\s+/, '');
|
||||
const safeText = trimmedLeft && /^[=+\-@]/.test(trimmedLeft) ? `'${text}` : text;
|
||||
return `"${safeText.replace(/"/g, '""')}"`;
|
||||
};
|
||||
|
||||
type CredentialInfo = {
|
||||
@@ -370,7 +363,10 @@ export function RequestEventsDetailsCard({
|
||||
|
||||
const content = [csvHeader.join(','), ...csvRows].join('\n');
|
||||
const fileTime = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
downloadFile(`usage-events-${fileTime}.csv`, content, 'text/csv;charset=utf-8');
|
||||
downloadBlob({
|
||||
filename: `usage-events-${fileTime}.csv`,
|
||||
blob: new Blob([content], { type: 'text/csv;charset=utf-8' })
|
||||
});
|
||||
};
|
||||
|
||||
const handleExportJson = () => {
|
||||
@@ -394,7 +390,10 @@ export function RequestEventsDetailsCard({
|
||||
|
||||
const content = JSON.stringify(payload, null, 2);
|
||||
const fileTime = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
downloadFile(`usage-events-${fileTime}.json`, content, 'application/json;charset=utf-8');
|
||||
downloadBlob({
|
||||
filename: `usage-events-${fileTime}.json`,
|
||||
blob: new Blob([content], { type: 'application/json;charset=utf-8' })
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { USAGE_STATS_STALE_TIME_MS, useNotificationStore, useUsageStatsStore } from '@/stores';
|
||||
import { usageApi } from '@/services/api/usage';
|
||||
import { downloadBlob } from '@/utils/download';
|
||||
import { loadModelPrices, saveModelPrices, type ModelPrice } from '@/utils/usage';
|
||||
|
||||
export interface UsagePayload {
|
||||
@@ -62,13 +63,10 @@ export function useUsageData(): UseUsageDataReturn {
|
||||
? new Date().toISOString()
|
||||
: exportedAt.toISOString();
|
||||
const filename = `usage-export-${safeTimestamp.replace(/[:.]/g, '-')}.json`;
|
||||
const blob = new Blob([JSON.stringify(data ?? {}, null, 2)], { type: 'application/json' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
downloadBlob({
|
||||
filename,
|
||||
blob: new Blob([JSON.stringify(data ?? {}, null, 2)], { type: 'application/json' })
|
||||
});
|
||||
showNotification(t('usage_stats.export_success'), 'success');
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '';
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useNotificationStore } from '@/stores';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { formatFileSize } from '@/utils/format';
|
||||
import { MAX_AUTH_FILE_SIZE } from '@/utils/constants';
|
||||
import { downloadBlob } from '@/utils/download';
|
||||
import { getTypeLabel, isRuntimeOnlyAuthFile } from '@/features/authFiles/constants';
|
||||
|
||||
type DeleteAllOptions = {
|
||||
@@ -316,12 +317,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
|
||||
{ responseType: 'blob' }
|
||||
);
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = name;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
downloadBlob({ filename: name, blob });
|
||||
showNotification(t('auth_files.download_success'), 'success');
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
|
||||
+7
-21
@@ -24,6 +24,7 @@ import { logsApi } from '@/services/api/logs';
|
||||
import { usageApi } from '@/services/api/usage';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { downloadBlob } from '@/utils/download';
|
||||
import { MANAGEMENT_API_PREFIX } from '@/utils/constants';
|
||||
import { formatUnixTimestamp } from '@/utils/format';
|
||||
import {
|
||||
@@ -710,13 +711,7 @@ export function LogsPage() {
|
||||
|
||||
const downloadLogs = () => {
|
||||
const text = logState.buffer.join('\n');
|
||||
const blob = new Blob([text], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'logs.txt';
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
downloadBlob({ filename: 'logs.txt', blob: new Blob([text], { type: 'text/plain' }) });
|
||||
showNotification(t('logs.download_success'), 'success');
|
||||
};
|
||||
|
||||
@@ -747,13 +742,7 @@ export function LogsPage() {
|
||||
const downloadErrorLog = async (name: string) => {
|
||||
try {
|
||||
const response = await logsApi.downloadErrorLog(name);
|
||||
const blob = new Blob([response.data], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = name;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
downloadBlob({ filename: name, blob: new Blob([response.data], { type: 'text/plain' }) });
|
||||
showNotification(t('logs.error_log_download_success'), 'success');
|
||||
} catch (err: unknown) {
|
||||
const message = getErrorMessage(err);
|
||||
@@ -1187,13 +1176,10 @@ export function LogsPage() {
|
||||
setRequestLogDownloading(true);
|
||||
try {
|
||||
const response = await logsApi.downloadRequestLogById(id);
|
||||
const blob = new Blob([response.data], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `request-${id}.log`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
downloadBlob({
|
||||
filename: `request-${id}.log`,
|
||||
blob: new Blob([response.data], { type: 'text/plain' })
|
||||
});
|
||||
showNotification(t('logs.request_log_download_success'), 'success');
|
||||
setRequestLogId(null);
|
||||
} catch (err: unknown) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export type DownloadBlobOptions = {
|
||||
filename: string;
|
||||
blob: Blob;
|
||||
revokeDelayMs?: number;
|
||||
};
|
||||
|
||||
export function downloadBlob({ filename, blob, revokeDelayMs = 1000 }: DownloadBlobOptions) {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.rel = 'noopener';
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
window.setTimeout(() => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
link.remove();
|
||||
}, revokeDelayMs);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user