feat: Add option to delete legacy data from disk
This commit is contained in:
10
README-zh.md
10
README-zh.md
@@ -132,6 +132,16 @@ MyTube 允许您将视频整理到收藏夹中:
|
||||
- **添加到收藏夹**:直接从视频播放器或管理页面将视频添加到一个或多个收藏夹。
|
||||
- **从收藏夹中移除**:轻松从收藏夹中移除视频。
|
||||
- **浏览收藏夹**:在侧边栏查看所有收藏夹,并按收藏夹浏览视频。
|
||||
- **删除选项**:选择仅删除收藏夹分组,或连同所有视频文件一起从磁盘删除。
|
||||
|
||||
## 数据迁移
|
||||
|
||||
MyTube 现在使用 SQLite 数据库以获得更好的性能和可靠性。如果您是从使用 JSON 文件的旧版本升级:
|
||||
|
||||
1. 进入 **设置**。
|
||||
2. 向下滚动到 **数据库** 部分。
|
||||
3. 点击 **从 JSON 迁移数据**。
|
||||
4. 该工具将把您现有的视频、收藏夹和下载历史导入到新数据库中。
|
||||
|
||||
## 用户界面
|
||||
|
||||
|
||||
10
README.md
10
README.md
@@ -133,6 +133,16 @@ MyTube allows you to organize your videos into collections:
|
||||
- **Add to Collections**: Add videos to one or more collections directly from the video player or manage page.
|
||||
- **Remove from Collections**: Remove videos from collections easily.
|
||||
- **Browse Collections**: View all your collections in the sidebar and browse videos by collection.
|
||||
- **Delete Options**: Choose to delete just the collection grouping or delete the collection along with all its video files from the disk.
|
||||
|
||||
## Data Migration
|
||||
|
||||
MyTube now uses a SQLite database for better performance and reliability. If you are upgrading from an older version that used JSON files:
|
||||
|
||||
1. Go to **Settings**.
|
||||
2. Scroll down to the **Database** section.
|
||||
3. Click **Migrate Data from JSON**.
|
||||
4. The tool will import your existing videos, collections, and download history into the new database.
|
||||
|
||||
## User Interface
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
CREATE TABLE `collection_videos` (
|
||||
CREATE TABLE IF NOT EXISTS `collection_videos` (
|
||||
`collection_id` text NOT NULL,
|
||||
`video_id` text NOT NULL,
|
||||
`order` integer,
|
||||
@@ -7,7 +7,7 @@ CREATE TABLE `collection_videos` (
|
||||
FOREIGN KEY (`video_id`) REFERENCES `videos`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `collections` (
|
||||
CREATE TABLE IF NOT EXISTS `collections` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`title` text,
|
||||
@@ -15,7 +15,7 @@ CREATE TABLE `collections` (
|
||||
`updated_at` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `downloads` (
|
||||
CREATE TABLE IF NOT EXISTS `downloads` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`timestamp` integer,
|
||||
@@ -27,12 +27,12 @@ CREATE TABLE `downloads` (
|
||||
`status` text DEFAULT 'active' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `settings` (
|
||||
CREATE TABLE IF NOT EXISTS `settings` (
|
||||
`key` text PRIMARY KEY NOT NULL,
|
||||
`value` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `videos` (
|
||||
CREATE TABLE IF NOT EXISTS `videos` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`author` text,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { Request, Response } from 'express';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { COLLECTIONS_DATA_PATH, STATUS_DATA_PATH, VIDEOS_DATA_PATH } from '../config/paths';
|
||||
import downloadManager from '../services/downloadManager';
|
||||
import * as storageService from '../services/storageService';
|
||||
|
||||
@@ -54,6 +57,40 @@ export const migrateData = async (req: Request, res: Response) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteLegacyData = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const SETTINGS_DATA_PATH = path.join(path.dirname(VIDEOS_DATA_PATH), 'settings.json');
|
||||
const filesToDelete = [
|
||||
VIDEOS_DATA_PATH,
|
||||
COLLECTIONS_DATA_PATH,
|
||||
STATUS_DATA_PATH,
|
||||
SETTINGS_DATA_PATH
|
||||
];
|
||||
|
||||
const results: { deleted: string[], failed: string[] } = {
|
||||
deleted: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
for (const file of filesToDelete) {
|
||||
if (fs.existsSync(file)) {
|
||||
try {
|
||||
fs.unlinkSync(file);
|
||||
results.deleted.push(path.basename(file));
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete ${file}:`, err);
|
||||
results.failed.push(path.basename(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, results });
|
||||
} catch (error: any) {
|
||||
console.error('Error deleting legacy data:', error);
|
||||
res.status(500).json({ error: 'Failed to delete legacy data', details: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSettings = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const newSettings: Settings = req.body;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from 'express';
|
||||
import { getSettings, migrateData, updateSettings, verifyPassword } from '../controllers/settingsController';
|
||||
import { deleteLegacyData, getSettings, migrateData, updateSettings, verifyPassword } from '../controllers/settingsController';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -7,5 +7,6 @@ router.get('/', getSettings);
|
||||
router.post('/', updateSettings);
|
||||
router.post('/verify-password', verifyPassword);
|
||||
router.post('/migrate', migrateData);
|
||||
router.post('/delete-legacy', deleteLegacyData);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -25,7 +25,9 @@ import {
|
||||
import axios from 'axios';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ConfirmationModal from '../components/ConfirmationModal';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { Language } from '../utils/translations';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
@@ -49,7 +51,8 @@ const SettingsPage: React.FC = () => {
|
||||
language: 'en'
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
|
||||
const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' | 'warning' | 'info' } | null>(null);
|
||||
const [showDeleteLegacyModal, setShowDeleteLegacyModal] = useState(false);
|
||||
const { t, setLanguage } = useLanguage();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -90,10 +93,10 @@ const SettingsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof Settings, value: any) => {
|
||||
const handleChange = (field: keyof Settings, value: string | boolean | number) => {
|
||||
setSettings(prev => ({ ...prev, [field]: value }));
|
||||
if (field === 'language') {
|
||||
setLanguage(value);
|
||||
setLanguage(value as Language);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -221,27 +224,26 @@ const SettingsPage: React.FC = () => {
|
||||
|
||||
{/* Database Settings */}
|
||||
<Grid size={12}>
|
||||
<Typography variant="h6" gutterBottom>Database</Typography>
|
||||
<Typography variant="h6" gutterBottom>{t('database')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
Migrate data from legacy JSON files to the new SQLite database.
|
||||
This action is safe to run multiple times (duplicates will be skipped).
|
||||
{t('migrateDataDescription')}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
onClick={async () => {
|
||||
if (window.confirm('Are you sure you want to migrate data? This may take a few moments.')) {
|
||||
if (window.confirm(t('migrateConfirmation'))) {
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await axios.post(`${API_URL}/settings/migrate`);
|
||||
const results = res.data.results;
|
||||
console.log('Migration results:', results);
|
||||
|
||||
let msg = 'Migration Report:\n';
|
||||
let msg = `${t('migrationReport')}:\n`;
|
||||
let hasData = false;
|
||||
|
||||
if (results.warnings && results.warnings.length > 0) {
|
||||
msg += `\n⚠️ WARNINGS:\n${results.warnings.join('\n')}\n`;
|
||||
msg += `\n⚠️ ${t('migrationWarnings')}:\n${results.warnings.join('\n')}\n`;
|
||||
}
|
||||
|
||||
const categories = ['videos', 'collections', 'settings', 'downloads'];
|
||||
@@ -249,28 +251,28 @@ const SettingsPage: React.FC = () => {
|
||||
const data = results[cat];
|
||||
if (data) {
|
||||
if (data.found) {
|
||||
msg += `\n✅ ${cat}: ${data.count} items migrated`;
|
||||
msg += `\n✅ ${cat}: ${data.count} ${t('itemsMigrated')}`;
|
||||
hasData = true;
|
||||
} else {
|
||||
msg += `\n❌ ${cat}: File not found at ${data.path}`;
|
||||
msg += `\n❌ ${cat}: ${t('fileNotFound')} ${data.path}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (results.errors && results.errors.length > 0) {
|
||||
msg += `\n\n⛔ ERRORS:\n${results.errors.join('\n')}`;
|
||||
msg += `\n\n⛔ ${t('migrationErrors')}:\n${results.errors.join('\n')}`;
|
||||
}
|
||||
|
||||
if (!hasData && (!results.errors || results.errors.length === 0)) {
|
||||
msg += '\n\n⚠️ No data files were found to migrate. Please check your volume mappings.';
|
||||
msg += `\n\n⚠️ ${t('noDataFilesFound')}`;
|
||||
}
|
||||
|
||||
alert(msg);
|
||||
setMessage({ text: hasData ? 'Migration completed. See details in alert.' : 'Migration finished but no data found.', type: hasData ? 'success' : 'warning' });
|
||||
setMessage({ text: hasData ? t('migrationSuccess') : t('migrationNoData'), type: hasData ? 'success' : 'warning' });
|
||||
} catch (error: any) {
|
||||
console.error('Migration failed:', error);
|
||||
setMessage({
|
||||
text: `Migration failed: ${error.response?.data?.details || error.message}`,
|
||||
text: `${t('migrationFailed')}: ${error.response?.data?.details || error.message}`,
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
@@ -280,8 +282,23 @@ const SettingsPage: React.FC = () => {
|
||||
}}
|
||||
disabled={saving}
|
||||
>
|
||||
Migrate Data from JSON
|
||||
{t('migrateDataButton')}
|
||||
</Button>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>{t('removeLegacyData')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
{t('removeLegacyDataDescription')}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setShowDeleteLegacyModal(true)}
|
||||
disabled={saving}
|
||||
>
|
||||
{t('deleteLegacyDataButton')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid size={12}>
|
||||
@@ -311,6 +328,41 @@ const SettingsPage: React.FC = () => {
|
||||
{message?.text}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<ConfirmationModal
|
||||
isOpen={showDeleteLegacyModal}
|
||||
onClose={() => setShowDeleteLegacyModal(false)}
|
||||
onConfirm={async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await axios.post(`${API_URL}/settings/delete-legacy`);
|
||||
const results = res.data.results;
|
||||
console.log('Delete legacy results:', results);
|
||||
|
||||
let msg = `${t('legacyDataDeleted')}\n`;
|
||||
if (results.deleted.length > 0) {
|
||||
msg += `\nDeleted: ${results.deleted.join(', ')}`;
|
||||
}
|
||||
if (results.failed.length > 0) {
|
||||
msg += `\nFailed: ${results.failed.join(', ')}`;
|
||||
}
|
||||
|
||||
alert(msg);
|
||||
setMessage({ text: t('legacyDataDeleted'), type: 'success' });
|
||||
} catch (error: any) {
|
||||
console.error('Failed to delete legacy data:', error);
|
||||
setMessage({
|
||||
text: `Failed to delete legacy data: ${error.response?.data?.details || error.message}`,
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}}
|
||||
title={t('removeLegacyDataConfirmTitle')}
|
||||
message={t('removeLegacyDataConfirmMessage')}
|
||||
confirmText={t('delete')}
|
||||
isDanger={true}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -48,6 +48,28 @@ export const translations = {
|
||||
settingsSaved: "Settings saved successfully",
|
||||
settingsFailed: "Failed to save settings",
|
||||
|
||||
// Database
|
||||
database: "Database",
|
||||
migrateDataDescription: "Migrate data from legacy JSON files to the new SQLite database. This action is safe to run multiple times (duplicates will be skipped).",
|
||||
migrateDataButton: "Migrate Data from JSON",
|
||||
migrateConfirmation: "Are you sure you want to migrate data? This may take a few moments.",
|
||||
migrationResults: "Migration Results",
|
||||
migrationReport: "Migration Report",
|
||||
migrationSuccess: "Migration completed. See details in alert.",
|
||||
migrationNoData: "Migration finished but no data found.",
|
||||
migrationFailed: "Migration failed",
|
||||
migrationWarnings: "WARNINGS",
|
||||
migrationErrors: "ERRORS",
|
||||
itemsMigrated: "items migrated",
|
||||
fileNotFound: "File not found at",
|
||||
noDataFilesFound: "No data files were found to migrate. Please check your volume mappings.",
|
||||
removeLegacyData: "Remove Legacy Data",
|
||||
removeLegacyDataDescription: "Delete the old JSON files (videos.json, collections.json, etc.) to clean up disk space. Only do this after verifying your data has been successfully migrated.",
|
||||
removeLegacyDataConfirmTitle: "Delete Legacy Data?",
|
||||
removeLegacyDataConfirmMessage: "Are you sure you want to delete the legacy JSON data files? This action cannot be undone.",
|
||||
legacyDataDeleted: "Legacy data deleted successfully.",
|
||||
deleteLegacyDataButton: "Delete Legacy Data",
|
||||
|
||||
// Manage
|
||||
manageContent: "Manage Content",
|
||||
videos: "Videos",
|
||||
@@ -222,6 +244,28 @@ export const translations = {
|
||||
settingsSaved: "设置保存成功",
|
||||
settingsFailed: "保存设置失败",
|
||||
|
||||
// Database
|
||||
database: "数据库",
|
||||
migrateDataDescription: "从旧版 JSON 文件迁移数据到新的 SQLite 数据库。此操作可以安全地多次运行(将跳过重复项)。",
|
||||
migrateDataButton: "从 JSON 迁移数据",
|
||||
migrateConfirmation: "确定要迁移数据吗?这可能需要一些时间。",
|
||||
migrationResults: "迁移结果",
|
||||
migrationReport: "迁移报告",
|
||||
migrationSuccess: "迁移完成。请查看警报中的详细信息。",
|
||||
migrationNoData: "迁移完成但未找到数据。",
|
||||
migrationFailed: "迁移失败",
|
||||
migrationWarnings: "警告",
|
||||
migrationErrors: "错误",
|
||||
itemsMigrated: "项已迁移",
|
||||
fileNotFound: "未找到文件于",
|
||||
noDataFilesFound: "未找到可迁移的数据文件。请检查您的卷映射。",
|
||||
removeLegacyData: "删除旧数据",
|
||||
removeLegacyDataDescription: "删除旧的 JSON 文件(videos.json, collections.json 等)以释放磁盘空间。请仅在确认数据已成功迁移后执行此操作。",
|
||||
removeLegacyDataConfirmTitle: "删除旧数据?",
|
||||
removeLegacyDataConfirmMessage: "确定要删除旧的 JSON 数据文件吗?此操作无法撤销。",
|
||||
legacyDataDeleted: "旧数据删除成功。",
|
||||
deleteLegacyDataButton: "删除旧数据",
|
||||
|
||||
// Manage
|
||||
manageContent: "内容管理",
|
||||
videos: "视频",
|
||||
|
||||
Reference in New Issue
Block a user