diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt index 05e1fed..dfff751 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt @@ -192,7 +192,6 @@ object LegacyBackupImport : LegacyBackupBase() { it[status] = fetchedManga.status if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty()) it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url - } } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt index 7f034de..899c38a 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt @@ -40,6 +40,8 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.future.future import mu.KotlinLogging import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Date import java.util.concurrent.CompletableFuture /* @@ -326,7 +328,7 @@ object JavalinSetup { ctx.json(getCategoryMangaList(categoryId)) } - // expects a Tachiyomi legacy backup json to be uploaded + // expects a Tachiyomi legacy backup json in the body app.post("/api/v1/backup/legacy/import") { ctx -> ctx.result( future { @@ -335,7 +337,16 @@ object JavalinSetup { ) } - // returns a Tachiyomi legacy backup json created from the current database + // expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json" + app.post("/api/v1/backup/legacy/import/file") { ctx -> + ctx.result( + future { + restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content) + } + ) + } + + // returns a Tachiyomi legacy backup json created from the current database as a json body app.get("/api/v1/backup/legacy/export") { ctx -> ctx.contentType("application/json") ctx.result( @@ -352,5 +363,27 @@ object JavalinSetup { } ) } + + // returns a Tachiyomi legacy backup json created from the current database as a file + app.get("/api/v1/backup/legacy/export/file") { ctx -> + ctx.contentType("application/json") + val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm") + val currentDate = sdf.format(Date()) + + ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"") + ctx.result( + future { + createLegacyBackup( + BackupFlags( + includeManga = true, + includeCategories = true, + includeChapters = true, + includeTracking = true, + includeHistory = true, + ) + ) + } + ) + } } } diff --git a/webUI/react/package.json b/webUI/react/package.json index c5e198a..58ae588 100644 --- a/webUI/react/package.json +++ b/webUI/react/package.json @@ -10,6 +10,7 @@ "@testing-library/user-event": "^12.1.10", "@types/react-lazyload": "^3.1.0", "axios": "^0.21.1", + "file-selector": "^0.2.4", "fontsource-roboto": "^4.0.0", "react": "^17.0.1", "react-beautiful-dnd": "^13.0.0", diff --git a/webUI/react/src/App.tsx b/webUI/react/src/App.tsx index 3cbaa05..0855514 100644 --- a/webUI/react/src/App.tsx +++ b/webUI/react/src/App.tsx @@ -25,6 +25,7 @@ import DarkTheme from './context/DarkTheme'; import Library from './screens/Library'; import Settings from './screens/Settings'; import Categories from './screens/settings/Categories'; +import Backup from './screens/settings/Backup'; import useLocalStorage from './util/useLocalStorage'; export default function App() { @@ -103,6 +104,9 @@ export default function App() { + + + diff --git a/webUI/react/src/screens/Settings.tsx b/webUI/react/src/screens/Settings.tsx index d1b6d67..0d02bd6 100644 --- a/webUI/react/src/screens/Settings.tsx +++ b/webUI/react/src/screens/Settings.tsx @@ -16,15 +16,11 @@ import { DialogContentText, IconButton, ListItemSecondaryAction, Switch, TextField, ListItemIcon, ListItemText, } from '@material-ui/core'; -import ListItem, { ListItemProps } from '@material-ui/core/ListItem'; +import ListItem from '@material-ui/core/ListItem'; import NavbarContext from '../context/NavbarContext'; import DarkTheme from '../context/DarkTheme'; import useLocalStorage from '../util/useLocalStorage'; - -function ListItemLink(props: ListItemProps<'a', { button?: true }>) { - // eslint-disable-next-line react/jsx-props-no-spreading - return ; -} +import ListItemLink from '../util/ListItemLink'; export default function Settings() { const { setTitle, setAction } = useContext(NavbarContext); @@ -58,6 +54,12 @@ export default function Settings() { + + + + + + diff --git a/webUI/react/src/screens/settings/Backup.tsx b/webUI/react/src/screens/settings/Backup.tsx new file mode 100644 index 0000000..ade387f --- /dev/null +++ b/webUI/react/src/screens/settings/Backup.tsx @@ -0,0 +1,91 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import React, { useContext, useEffect } from 'react'; +import { ListItemIcon } from '@material-ui/core'; +import List from '@material-ui/core/List'; +import InboxIcon from '@material-ui/icons/Inbox'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; +import { fromEvent } from 'file-selector'; +import ListItemLink from '../../util/ListItemLink'; +import NavbarContext from '../../context/NavbarContext'; +import client from '../../util/client'; + +export default function Backup() { + const { setTitle, setAction } = useContext(NavbarContext); + useEffect(() => { setTitle('Backup'); setAction(<>); }, []); + + const { baseURL } = client.defaults; + + const submitBackup = (file: File) => { + file.text() + .then( + (fileContent: string) => { + client.post('/api/v1/backup/legacy/import', + fileContent, { headers: { 'Content-Type': 'application/json' } }); + }, + ); + }; + + const dropHandler = async (e: Event) => { + e.preventDefault(); + const files = await fromEvent(e); + + submitBackup(files[0] as File); + }; + + const dragOverHandler = (e: Event) => { + e.preventDefault(); + }; + + useEffect(() => { + document.addEventListener('drop', dropHandler); + document.addEventListener('dragover', dragOverHandler); + + const input = document.getElementById('backup-file'); + input?.addEventListener('change', async (evt) => { + const files = await fromEvent(evt); + submitBackup(files[0] as File); + }); + + return () => { + document.removeEventListener('drop', dropHandler); + document.removeEventListener('dragover', dragOverHandler); + }; + }, []); + + return ( + + + + + + + + document.getElementById('backup-file')?.click()}> + + + + + + + + + ); +} diff --git a/webUI/react/src/util/ListItemLink.tsx b/webUI/react/src/util/ListItemLink.tsx new file mode 100644 index 0000000..fc1b6b9 --- /dev/null +++ b/webUI/react/src/util/ListItemLink.tsx @@ -0,0 +1,14 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import React from 'react'; +import ListItem, { ListItemProps } from '@material-ui/core/ListItem'; + +export default function ListItemLink(props: ListItemProps<'a', { button?: true }>) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} diff --git a/webUI/react/yarn.lock b/webUI/react/yarn.lock index c0e01d0..941059c 100644 --- a/webUI/react/yarn.lock +++ b/webUI/react/yarn.lock @@ -5136,6 +5136,13 @@ file-loader@6.1.1: loader-utils "^2.0.0" schema-utils "^3.0.0" +file-selector@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.4.tgz#7b98286f9dbb9925f420130ea5ed0a69238d4d80" + integrity sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA== + dependencies: + tslib "^2.0.3" + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"