mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-06 20:42:57 +08:00
🚧 WIP(custom): support sync gallery db file
ISSUES CLOSED: #355,#417
This commit is contained in:
@@ -59,6 +59,7 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"fast-xml-parser": "^5.3.3",
|
"fast-xml-parser": "^5.3.3",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.3",
|
||||||
"got": "^14.6.5",
|
"got": "^14.6.5",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { app } from 'electron'
|
|||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
|
|
||||||
import { IRPCActionType, IRPCType } from '~/utils/enum'
|
import { IRPCActionType, IRPCType } from '~/utils/enum'
|
||||||
import { downloadFile, uploadFile } from '~/utils/syncSettings'
|
import { downloadFile, syncGallery, uploadFile } from '~/utils/syncSettings'
|
||||||
|
|
||||||
const STORE_PATH = app.getPath('userData')
|
const STORE_PATH = app.getPath('userData')
|
||||||
|
|
||||||
@@ -48,6 +48,13 @@ export default [
|
|||||||
},
|
},
|
||||||
type: IRPCType.INVOKE,
|
type: IRPCType.INVOKE,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
action: IRPCActionType.CONFIGURE_SYNC_GALLERY_DB,
|
||||||
|
handler: async () => {
|
||||||
|
return await syncGallery()
|
||||||
|
},
|
||||||
|
type: IRPCType.INVOKE,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
action: IRPCActionType.CONFIGURE_UPLOAD_ALL_CONFIG,
|
action: IRPCActionType.CONFIGURE_UPLOAD_ALL_CONFIG,
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import db from '@core/datastore'
|
|||||||
import logger from '@core/picgo/logger'
|
import logger from '@core/picgo/logger'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { clipboard, Notification, Tray } from 'electron'
|
import { clipboard, Notification, Tray } from 'electron'
|
||||||
|
import { gunzipSync, gzipSync, strFromU8 } from 'fflate'
|
||||||
import FormData from 'form-data'
|
import FormData from 'form-data'
|
||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
import { isReactive, isRef, toRaw, unref } from 'vue'
|
import { isReactive, isRef, toRaw, unref } from 'vue'
|
||||||
@@ -321,3 +322,23 @@ export function encodeFilePath(filePath: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const trimPath = (path: string) => path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/')
|
export const trimPath = (path: string) => path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/')
|
||||||
|
|
||||||
|
export const extractData = async (zipPath: string): Promise<Record<string, any>> => {
|
||||||
|
try {
|
||||||
|
const buffer = await fs.readFile(zipPath)
|
||||||
|
const str = strFromU8(gunzipSync(buffer))
|
||||||
|
return JSON.parse(str)
|
||||||
|
} catch (_err) {
|
||||||
|
throw new Error('Extract failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const zipData = async (data: Record<string, any>, zipPath: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.from(JSON.stringify(data))
|
||||||
|
const compressed = gzipSync(buffer)
|
||||||
|
await fs.writeFile(zipPath, Buffer.from(compressed))
|
||||||
|
} catch (_err) {
|
||||||
|
throw new Error('Zip failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export const IRPCActionType = {
|
|||||||
CONFIGURE_MIGRATE_FROM_PICGO: 'CONFIGURE_MIGRATE_FROM_PICGO',
|
CONFIGURE_MIGRATE_FROM_PICGO: 'CONFIGURE_MIGRATE_FROM_PICGO',
|
||||||
CONFIGURE_UPLOAD_COMMON_CONFIG: 'CONFIGURE_UPLOAD_COMMON_CONFIG',
|
CONFIGURE_UPLOAD_COMMON_CONFIG: 'CONFIGURE_UPLOAD_COMMON_CONFIG',
|
||||||
CONFIGURE_UPLOAD_MANAGE_CONFIG: 'CONFIGURE_UPLOAD_MANAGE_CONFIG',
|
CONFIGURE_UPLOAD_MANAGE_CONFIG: 'CONFIGURE_UPLOAD_MANAGE_CONFIG',
|
||||||
|
CONFIGURE_SYNC_GALLERY_DB: 'CONFIGURE_SYNC_GALLERY_DB',
|
||||||
CONFIGURE_UPLOAD_ALL_CONFIG: 'CONFIGURE_UPLOAD_ALL_CONFIG',
|
CONFIGURE_UPLOAD_ALL_CONFIG: 'CONFIGURE_UPLOAD_ALL_CONFIG',
|
||||||
CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG',
|
CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG',
|
||||||
CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG',
|
CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os from 'node:os'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
import db from '@core/datastore'
|
import db from '@core/datastore'
|
||||||
@@ -10,32 +11,71 @@ import { HttpsProxyAgent } from 'hpagent'
|
|||||||
import { AuthType, createClient, WebDAVClientOptions } from 'webdav'
|
import { AuthType, createClient, WebDAVClientOptions } from 'webdav'
|
||||||
|
|
||||||
import type { ISyncConfig } from '#/types/types'
|
import type { ISyncConfig } from '#/types/types'
|
||||||
|
import { extractData, zipData } from '~/utils/common'
|
||||||
import { formatEndpoint } from '~/utils/common'
|
import { formatEndpoint } from '~/utils/common'
|
||||||
import { configPaths } from '~/utils/configPaths'
|
import { configPaths } from '~/utils/configPaths'
|
||||||
|
|
||||||
const STORE_PATH = app.getPath('userData')
|
const STORE_PATH = app.getPath('userData')
|
||||||
|
const tempDir = path.join(os.tmpdir(), `piclist-sync-tmp`)
|
||||||
|
const db1 = path.join(tempDir, 'db1')
|
||||||
|
const db2 = path.join(tempDir, 'db2')
|
||||||
|
const dbMerged = path.join(tempDir, 'db-merged')
|
||||||
|
const galleryDBList = ['piclist.db', 'piclist.bak.db']
|
||||||
|
|
||||||
const readFileAsBase64 = (filePath: string) => fs.readFileSync(filePath, { encoding: 'base64' })
|
const readFileAsBase64 = (filePath: string) => fs.readFileSync(filePath, { encoding: 'base64' })
|
||||||
|
|
||||||
const isHttpResSuccess = (res: any) => res.status >= 200 && res.status < 300
|
const isHttpResSuccess = (res: any) => res.status >= 200 && res.status < 300
|
||||||
|
|
||||||
const uploadOrUpdateMsg = (fileName: string, isUpdate: boolean = true) =>
|
const uploadOrUpdateMsg = (fileName: string, isUpdate: boolean = true) =>
|
||||||
isUpdate ? `update ${fileName} from PicList` : `upload ${fileName} from PicList`
|
isUpdate ? `update ${fileName} from PicList` : `upload ${fileName} from PicList`
|
||||||
|
|
||||||
const getSyncConfig = () => {
|
const emptyDir = async (): Promise<void> => {
|
||||||
return (
|
await fs.emptyDir(tempDir)
|
||||||
db.get(configPaths.settings.sync) || {
|
await fs.emptyDir(db1)
|
||||||
type: 'github',
|
await fs.emptyDir(db2)
|
||||||
username: '',
|
await fs.emptyDir(dbMerged)
|
||||||
repo: '',
|
|
||||||
branch: '',
|
|
||||||
token: '',
|
|
||||||
proxy: '',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getProxyagent = (proxy: string | undefined) => {
|
const mergeGalleryDB = async (targetFile: string) => {
|
||||||
return proxy
|
try {
|
||||||
|
const db1Data = await extractData(path.join(db1, targetFile))
|
||||||
|
const db2Data = await extractData(path.join(db2, targetFile))
|
||||||
|
const mergedData: any = {
|
||||||
|
gallery: [],
|
||||||
|
__gallery_KEY__: {},
|
||||||
|
}
|
||||||
|
const db1Ids = new Set<string>(Object.keys(db1Data.__gallery_KEY__ || {}))
|
||||||
|
const db2Ids = new Set<string>(Object.keys(db2Data.__gallery_KEY__ || {}))
|
||||||
|
const idSet = new Set<string>([...db1Ids, ...db2Ids])
|
||||||
|
for (const id of idSet) {
|
||||||
|
if (db2Ids.has(id)) {
|
||||||
|
mergedData.gallery.push(db2Data.gallery.find((item: any) => item.id === id))
|
||||||
|
} else if (db1Ids.has(id)) {
|
||||||
|
mergedData.gallery.push(db1Data.gallery.find((item: any) => item.id === id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const item of mergedData.gallery) {
|
||||||
|
mergedData.__gallery_KEY__[item.id] = 1
|
||||||
|
}
|
||||||
|
await zipData(mergedData, path.join(dbMerged, targetFile))
|
||||||
|
await fs.copyFile(path.join(dbMerged, targetFile), path.join(STORE_PATH, targetFile))
|
||||||
|
} catch (err: any) {
|
||||||
|
logger.error('merge gallery db failed:', String(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSyncConfig = () =>
|
||||||
|
db.get(configPaths.settings.sync) || {
|
||||||
|
type: 'github',
|
||||||
|
username: '',
|
||||||
|
repo: '',
|
||||||
|
branch: '',
|
||||||
|
token: '',
|
||||||
|
proxy: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProxyagent = (proxy: string | undefined) =>
|
||||||
|
proxy
|
||||||
? new HttpsProxyAgent({
|
? new HttpsProxyAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 1000,
|
keepAliveMsecs: 1000,
|
||||||
@@ -44,17 +84,14 @@ const getProxyagent = (proxy: string | undefined) => {
|
|||||||
scheduling: 'lifo',
|
scheduling: 'lifo',
|
||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
}
|
|
||||||
|
|
||||||
function getOctokit(syncConfig: ISyncConfig) {
|
const getOctokit = (syncConfig: ISyncConfig) =>
|
||||||
const { token, proxy } = syncConfig
|
new Octokit({
|
||||||
return new Octokit({
|
auth: syncConfig.token,
|
||||||
auth: token,
|
|
||||||
request: {
|
request: {
|
||||||
agent: getProxyagent(proxy),
|
agent: getProxyagent(syncConfig.proxy),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const isSyncConfigValidate = ({
|
const isSyncConfigValidate = ({
|
||||||
type,
|
type,
|
||||||
@@ -85,10 +122,10 @@ const isSyncConfigValidate = ({
|
|||||||
|
|
||||||
async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
|
async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
|
||||||
const localFilePath = path.join(STORE_PATH, fileName)
|
const localFilePath = path.join(STORE_PATH, fileName)
|
||||||
if (!fs.existsSync(localFilePath)) {
|
if (!fs.existsSync(localFilePath)) return false
|
||||||
return false
|
|
||||||
}
|
|
||||||
const { username, repo, branch, token, type } = syncConfig
|
const { username, repo, branch, token, type } = syncConfig
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
content: readFileAsBase64(localFilePath),
|
content: readFileAsBase64(localFilePath),
|
||||||
message: uploadOrUpdateMsg(fileName, false),
|
message: uploadOrUpdateMsg(fileName, false),
|
||||||
@@ -97,8 +134,7 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
|
|||||||
try {
|
try {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'gitee': {
|
case 'gitee': {
|
||||||
const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}`
|
const res = await axios.post(`https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}`, {
|
||||||
const res = await axios.post(url, {
|
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
access_token: token,
|
access_token: token,
|
||||||
})
|
})
|
||||||
@@ -116,11 +152,15 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
|
|||||||
}
|
}
|
||||||
case 'gitea': {
|
case 'gitea': {
|
||||||
const { endpoint = '' } = syncConfig
|
const { endpoint = '' } = syncConfig
|
||||||
const apiUrl = `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}`
|
const res = await axios.post(
|
||||||
const headers = {
|
`${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}`,
|
||||||
Authorization: `token ${token}`,
|
defaultConfig,
|
||||||
}
|
{
|
||||||
const res = await axios.post(apiUrl, defaultConfig, { headers })
|
headers: {
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
return isHttpResSuccess(res)
|
return isHttpResSuccess(res)
|
||||||
}
|
}
|
||||||
case 'webdav': {
|
case 'webdav': {
|
||||||
@@ -136,9 +176,7 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
|
|||||||
const options: WebDAVClientOptions = {
|
const options: WebDAVClientOptions = {
|
||||||
username: webdavUsername,
|
username: webdavUsername,
|
||||||
password: webdavPassword,
|
password: webdavPassword,
|
||||||
}
|
...(webdavAuthType === 'digest' ? { authType: AuthType.Digest } : {}),
|
||||||
if (webdavAuthType === 'digest') {
|
|
||||||
options.authType = AuthType.Digest
|
|
||||||
}
|
}
|
||||||
const client = createClient(webdavEndpointF, options)
|
const client = createClient(webdavEndpointF, options)
|
||||||
const fileContent = fs.readFileSync(localFilePath)
|
const fileContent = fs.readFileSync(localFilePath)
|
||||||
@@ -161,6 +199,31 @@ async function uploadLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function uploadFile(fileName: string[]): Promise<number> {
|
||||||
|
const syncConfig = getSyncConfig()
|
||||||
|
if (!isSyncConfigValidate(syncConfig)) {
|
||||||
|
logger.error('sync config is invalid')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const uploadFunc = async (file: string): Promise<number> => {
|
||||||
|
let result = false
|
||||||
|
try {
|
||||||
|
result = await updateLocalToRemote(syncConfig, file)
|
||||||
|
} catch (_e: any) {
|
||||||
|
result = await uploadLocalToRemote(syncConfig, file)
|
||||||
|
}
|
||||||
|
logger.info(`upload ${file} ${result ? 'success' : 'failed'}`)
|
||||||
|
return result ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
for (const file of fileName) {
|
||||||
|
count += await uploadFunc(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
async function updateLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
|
async function updateLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
|
||||||
const localFilePath = path.join(STORE_PATH, fileName)
|
const localFilePath = path.join(STORE_PATH, fileName)
|
||||||
if (!fs.existsSync(localFilePath)) {
|
if (!fs.existsSync(localFilePath)) {
|
||||||
@@ -277,31 +340,6 @@ async function updateLocalToRemote(syncConfig: ISyncConfig, fileName: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFile(fileName: string[]): Promise<number> {
|
|
||||||
const syncConfig = getSyncConfig()
|
|
||||||
if (!isSyncConfigValidate(syncConfig)) {
|
|
||||||
logger.error('sync config is invalid')
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
const uploadFunc = async (file: string): Promise<number> => {
|
|
||||||
let result = false
|
|
||||||
try {
|
|
||||||
result = await updateLocalToRemote(syncConfig, file)
|
|
||||||
} catch (_e: any) {
|
|
||||||
result = await uploadLocalToRemote(syncConfig, file)
|
|
||||||
}
|
|
||||||
logger.info(`upload ${file} ${result ? 'success' : 'failed'}`)
|
|
||||||
return result ? 1 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
let count = 0
|
|
||||||
for (const file of fileName) {
|
|
||||||
count += await uploadFunc(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadAndWriteFile(url: string, localFilePath: string, config: any, isWriteJson = false) {
|
async function downloadAndWriteFile(url: string, localFilePath: string, config: any, isWriteJson = false) {
|
||||||
const res = await axios.get(url, config)
|
const res = await axios.get(url, config)
|
||||||
if (isHttpResSuccess(res)) {
|
if (isHttpResSuccess(res)) {
|
||||||
@@ -314,19 +352,31 @@ async function downloadAndWriteFile(url: string, localFilePath: string, config:
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string) {
|
async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string, galleryMode = false) {
|
||||||
const localFilePath = path.join(STORE_PATH, fileName)
|
const storePath = galleryMode ? db2 : STORE_PATH
|
||||||
|
const localFilePath = path.join(storePath, fileName)
|
||||||
const { username, repo, branch, token, proxy, type } = syncConfig
|
const { username, repo, branch, token, proxy, type } = syncConfig
|
||||||
try {
|
try {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'gitee': {
|
case 'gitee': {
|
||||||
const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}`
|
const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}`
|
||||||
return downloadAndWriteFile(url, localFilePath, {
|
const config = {
|
||||||
params: {
|
params: { access_token: token, ref: branch },
|
||||||
access_token: token,
|
}
|
||||||
ref: branch,
|
if (galleryMode) {
|
||||||
},
|
const res = await axios.get(url, config)
|
||||||
})
|
if (isHttpResSuccess(res)) {
|
||||||
|
const downloadUrl = res.data.download_url
|
||||||
|
const fileRes = await axios.get(downloadUrl, { responseType: 'arraybuffer' })
|
||||||
|
if (isHttpResSuccess(fileRes)) {
|
||||||
|
await fs.writeFile(localFilePath, fileRes.data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return downloadAndWriteFile(url, localFilePath, config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case 'github': {
|
case 'github': {
|
||||||
const octokit = getOctokit(syncConfig)
|
const octokit = getOctokit(syncConfig)
|
||||||
@@ -339,28 +389,59 @@ async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string)
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const data = res.data as any
|
const data = res.data as any
|
||||||
const downloadUrl = data.download_url
|
const downloadUrl = data.download_url
|
||||||
return downloadAndWriteFile(
|
if (galleryMode) {
|
||||||
downloadUrl,
|
const res = await axios.get(downloadUrl, {
|
||||||
localFilePath,
|
|
||||||
{
|
|
||||||
httpsAgent: getProxyagent(proxy),
|
httpsAgent: getProxyagent(proxy),
|
||||||
},
|
responseType: 'arraybuffer',
|
||||||
true,
|
})
|
||||||
)
|
if (isHttpResSuccess(res)) {
|
||||||
|
await fs.writeFile(localFilePath, res.data)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return downloadAndWriteFile(
|
||||||
|
downloadUrl,
|
||||||
|
localFilePath,
|
||||||
|
{
|
||||||
|
httpsAgent: getProxyagent(proxy),
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case 'gitea': {
|
case 'gitea': {
|
||||||
const { endpoint = '' } = syncConfig
|
const { endpoint = '', token, username, repo, branch } = syncConfig
|
||||||
const apiUrl = `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}`
|
if (galleryMode) {
|
||||||
return downloadAndWriteFile(apiUrl, localFilePath, {
|
const rawUrl = `${endpoint}/api/v1/repos/${username}/${repo}/raw/${fileName}`
|
||||||
headers: {
|
const res = await axios.get(rawUrl, {
|
||||||
Authorization: `token ${token}`,
|
headers: {
|
||||||
},
|
Authorization: `token ${token}`,
|
||||||
params: {
|
},
|
||||||
ref: branch,
|
params: {
|
||||||
},
|
ref: branch,
|
||||||
})
|
},
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
})
|
||||||
|
if (isHttpResSuccess(res)) {
|
||||||
|
await fs.writeFile(localFilePath, res.data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
const apiUrl = `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}`
|
||||||
|
return downloadAndWriteFile(apiUrl, localFilePath, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
ref: branch,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case 'webdav': {
|
case 'webdav': {
|
||||||
const {
|
const {
|
||||||
@@ -394,6 +475,82 @@ async function downloadRemoteToLocal(syncConfig: ISyncConfig, fileName: string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkCloudFileExist(syncConfig: ISyncConfig, fileName: string) {
|
||||||
|
const { username, repo, branch, token, type } = syncConfig
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case 'gitee': {
|
||||||
|
const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${fileName}`
|
||||||
|
try {
|
||||||
|
const res = await axios.get(url, {
|
||||||
|
params: { access_token: token, ref: branch },
|
||||||
|
})
|
||||||
|
return isHttpResSuccess(res)
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.status === 404) return false
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'github': {
|
||||||
|
const octokit = getOctokit(syncConfig)
|
||||||
|
try {
|
||||||
|
const res = await octokit.rest.repos.getContent({
|
||||||
|
owner: username,
|
||||||
|
repo,
|
||||||
|
path: fileName,
|
||||||
|
ref: branch,
|
||||||
|
})
|
||||||
|
return res.status === 200
|
||||||
|
} catch (error: any) {
|
||||||
|
if (Number(error.status) === 404) return false
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'gitea': {
|
||||||
|
const { endpoint = '' } = syncConfig
|
||||||
|
const apiUrl = `${endpoint}/api/v1/repos/${username}/${repo}/contents/${fileName}`
|
||||||
|
try {
|
||||||
|
const res = await axios.get(apiUrl, {
|
||||||
|
headers: { Authorization: `token ${token}` },
|
||||||
|
params: { ref: branch },
|
||||||
|
})
|
||||||
|
return isHttpResSuccess(res)
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.status === 404) return false
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'webdav': {
|
||||||
|
const {
|
||||||
|
webdavEndpoint = '',
|
||||||
|
webdavUsername,
|
||||||
|
webdavPassword,
|
||||||
|
webdavAuthType = 'basic',
|
||||||
|
webdavSslEnabled = true,
|
||||||
|
webdavSavePath = '',
|
||||||
|
} = syncConfig
|
||||||
|
const webdavEndpointF = formatEndpoint(webdavEndpoint, webdavSslEnabled)
|
||||||
|
const options: WebDAVClientOptions = {
|
||||||
|
username: webdavUsername,
|
||||||
|
password: webdavPassword,
|
||||||
|
}
|
||||||
|
if (webdavAuthType === 'digest') {
|
||||||
|
options.authType = AuthType.Digest
|
||||||
|
}
|
||||||
|
const client = createClient(webdavEndpointF, options)
|
||||||
|
const remoteFilePath = (webdavSavePath ? path.join(webdavSavePath, fileName) : fileName).replace(/\\/g, '/')
|
||||||
|
const exists = await client.exists(remoteFilePath)
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('unsupported sync type')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(error)
|
||||||
|
throw new Error('check file exist failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadFile(fileName: string[]): Promise<number> {
|
async function downloadFile(fileName: string[]): Promise<number> {
|
||||||
const syncConfig = getSyncConfig()
|
const syncConfig = getSyncConfig()
|
||||||
if (!isSyncConfigValidate(syncConfig)) {
|
if (!isSyncConfigValidate(syncConfig)) {
|
||||||
@@ -410,4 +567,35 @@ async function downloadFile(fileName: string[]): Promise<number> {
|
|||||||
return (await Promise.all(fileName.map(downloadFunc))).reduce((a, b) => a + b, 0)
|
return (await Promise.all(fileName.map(downloadFunc))).reduce((a, b) => a + b, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { downloadFile, uploadFile }
|
async function syncGallery(): Promise<number> {
|
||||||
|
const syncConfig = getSyncConfig()
|
||||||
|
if (!isSyncConfigValidate(syncConfig)) {
|
||||||
|
logger.error('sync config is invalid')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
let successCount = 0
|
||||||
|
for (const file of galleryDBList) {
|
||||||
|
await emptyDir()
|
||||||
|
try {
|
||||||
|
const exists = await checkCloudFileExist(syncConfig, file)
|
||||||
|
if (!exists) {
|
||||||
|
await uploadLocalToRemote(syncConfig, file)
|
||||||
|
logger.info(`gallery db ${file} not exist in cloud, upload local file instead`)
|
||||||
|
successCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
logger.error(`check gallery db ${file} exist failed:`, String(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await downloadRemoteToLocal(syncConfig, file, true)
|
||||||
|
await fs.copyFile(path.join(STORE_PATH, file), path.join(db1, file))
|
||||||
|
await mergeGalleryDB(file)
|
||||||
|
await updateLocalToRemote(syncConfig, file)
|
||||||
|
logger.info(`sync gallery db ${file} success`)
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
return successCount
|
||||||
|
}
|
||||||
|
|
||||||
|
export { downloadFile, syncGallery, uploadFile }
|
||||||
|
|||||||
@@ -685,6 +685,7 @@
|
|||||||
"commonConfig": "Common Configuration",
|
"commonConfig": "Common Configuration",
|
||||||
"downloadSettings": "Download Settings",
|
"downloadSettings": "Download Settings",
|
||||||
"fileManagement": "File Management",
|
"fileManagement": "File Management",
|
||||||
|
"galleryDB": "Gallery Database Sync",
|
||||||
"gitea": {
|
"gitea": {
|
||||||
"branch": "Branch Name",
|
"branch": "Branch Name",
|
||||||
"repo": "Repository Name",
|
"repo": "Repository Name",
|
||||||
|
|||||||
@@ -685,6 +685,7 @@
|
|||||||
"commonConfig": "通用配置",
|
"commonConfig": "通用配置",
|
||||||
"downloadSettings": "下载配置",
|
"downloadSettings": "下载配置",
|
||||||
"fileManagement": "文件管理",
|
"fileManagement": "文件管理",
|
||||||
|
"galleryDB": "相册数据库同步",
|
||||||
"gitea": { "branch": "分支名", "repo": "仓库名", "token": "访问令牌", "username": "用户名" },
|
"gitea": { "branch": "分支名", "repo": "仓库名", "token": "访问令牌", "username": "用户名" },
|
||||||
"giteaHost": "Gitea 地址",
|
"giteaHost": "Gitea 地址",
|
||||||
"gitee": {
|
"gitee": {
|
||||||
|
|||||||
@@ -685,6 +685,7 @@
|
|||||||
"commonConfig": "通用配置",
|
"commonConfig": "通用配置",
|
||||||
"downloadSettings": "下載配置",
|
"downloadSettings": "下載配置",
|
||||||
"fileManagement": "文件管理",
|
"fileManagement": "文件管理",
|
||||||
|
"galleryDB": "相冊數據庫同步",
|
||||||
"gitea": { "branch": "分支名", "repo": "倉庫名", "token": "訪問令牌", "username": "用戶名" },
|
"gitea": { "branch": "分支名", "repo": "倉庫名", "token": "訪問令牌", "username": "用戶名" },
|
||||||
"giteaHost": "Gitea 地址",
|
"giteaHost": "Gitea 地址",
|
||||||
"gitee": {
|
"gitee": {
|
||||||
|
|||||||
@@ -1224,37 +1224,70 @@
|
|||||||
|
|
||||||
<!-- Upload/Download Config Dialog -->
|
<!-- Upload/Download Config Dialog -->
|
||||||
<div v-if="upDownConfigVisible" class="dialog-overlay" @click="upDownConfigVisible = false">
|
<div v-if="upDownConfigVisible" class="dialog-overlay" @click="upDownConfigVisible = false">
|
||||||
<div class="dialog" @click.stop>
|
<div class="dialog config-dialog" @click.stop>
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
<h3 class="dialog-title">
|
<div class="dialog-header-content">
|
||||||
{{ t('pages.settings.sync.upDownloadSettings') }}
|
<RotateCcw :size="20" class="dialog-icon" />
|
||||||
</h3>
|
<h3 class="dialog-title">
|
||||||
|
{{ t('pages.settings.sync.upDownloadSettings') }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
<button class="dialog-close" @click="upDownConfigVisible = false">×</button>
|
<button class="dialog-close" @click="upDownConfigVisible = false">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-content">
|
<div class="dialog-content">
|
||||||
<div class="form-group">
|
<!-- Upload Settings Section -->
|
||||||
<label>{{ t('pages.settings.sync.uploadSettings') }}</label>
|
<div class="config-section">
|
||||||
<div class="button-group">
|
<div class="config-section-header">
|
||||||
|
<CloudUpload :size="18" />
|
||||||
|
<h4>{{ t('pages.settings.sync.uploadSettings') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="config-button-grid">
|
||||||
<button
|
<button
|
||||||
v-for="item in syncTaskList.slice(0, 3)"
|
v-for="item in syncTaskList.slice(0, 3)"
|
||||||
:key="item.task"
|
:key="item.task"
|
||||||
class="btn btn-primary"
|
class="config-button"
|
||||||
@click="syncTaskFn(item.task, item.number)"
|
@click="syncTaskFn(item.task, item.number)"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
<Import :size="16" class="button-icon" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label>{{ t('pages.settings.sync.downloadSettings') }}</label>
|
<!-- Download Settings Section -->
|
||||||
<div class="button-group">
|
<div class="config-section">
|
||||||
|
<div class="config-section-header">
|
||||||
|
<Download :size="18" />
|
||||||
|
<h4>{{ t('pages.settings.sync.downloadSettings') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="config-button-grid">
|
||||||
<button
|
<button
|
||||||
v-for="item in syncTaskList.slice(3)"
|
v-for="item in syncTaskList.slice(3, 6)"
|
||||||
:key="item.task"
|
:key="item.task"
|
||||||
class="btn btn-primary"
|
class="config-button"
|
||||||
@click="syncTaskFn(item.task, item.number)"
|
@click="syncTaskFn(item.task, item.number)"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
<Download :size="16" class="button-icon" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gallery DB Section -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="config-section-header">
|
||||||
|
<ImageIcon :size="18" />
|
||||||
|
<h4>{{ t('pages.settings.sync.galleryDB') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="config-button-grid full-width">
|
||||||
|
<button
|
||||||
|
v-for="item in syncTaskList.slice(6, 7)"
|
||||||
|
:key="item.task"
|
||||||
|
class="config-button"
|
||||||
|
@click="syncTaskFn(item.task, item.number)"
|
||||||
|
>
|
||||||
|
<RefreshCw :size="16" class="button-icon" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1940,6 +1973,7 @@ const syncTaskList = [
|
|||||||
{ task: IRPCActionType.CONFIGURE_DOWNLOAD_COMMON_CONFIG, label: t('pages.settings.sync.commonConfig'), number: 2 },
|
{ task: IRPCActionType.CONFIGURE_DOWNLOAD_COMMON_CONFIG, label: t('pages.settings.sync.commonConfig'), number: 2 },
|
||||||
{ task: IRPCActionType.CONFIGURE_DOWNLOAD_MANAGE_CONFIG, label: t('pages.settings.sync.manageConfig'), number: 2 },
|
{ task: IRPCActionType.CONFIGURE_DOWNLOAD_MANAGE_CONFIG, label: t('pages.settings.sync.manageConfig'), number: 2 },
|
||||||
{ task: IRPCActionType.CONFIGURE_DOWNLOAD_ALL_CONFIG, label: t('pages.settings.sync.allConfig'), number: 4 },
|
{ task: IRPCActionType.CONFIGURE_DOWNLOAD_ALL_CONFIG, label: t('pages.settings.sync.allConfig'), number: 4 },
|
||||||
|
{ task: IRPCActionType.CONFIGURE_SYNC_GALLERY_DB, label: t('pages.settings.sync.galleryDB'), number: 2 },
|
||||||
]
|
]
|
||||||
|
|
||||||
async function syncTaskFn(task: string, number: number) {
|
async function syncTaskFn(task: string, number: number) {
|
||||||
|
|||||||
@@ -964,3 +964,121 @@ small {
|
|||||||
.rotate {
|
.rotate {
|
||||||
animation: rotate 1s linear infinite;
|
animation: rotate 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Config Dialog Styles */
|
||||||
|
.config-dialog {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-icon {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
background: var(--color-background-secondary);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section:hover {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 2px 12px rgb(64 158 255 / 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section:not(:last-child) {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-header svg {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-button-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-button-grid.full-width {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
background: var(--color-background-primary);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-button:hover {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
color: var(--color-accent);
|
||||||
|
background: var(--color-background-hover);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgb(64 158 255 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 6px rgb(64 158 255 / 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-button .button-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--color-accent);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-button:hover .button-icon {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-button span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 768px) {
|
||||||
|
.config-button-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog {
|
||||||
|
max-width: 95%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export const IRPCActionType = {
|
|||||||
CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG',
|
CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG',
|
||||||
CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG',
|
CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG',
|
||||||
CONFIGURE_DOWNLOAD_ALL_CONFIG: 'CONFIGURE_DOWNLOAD_ALL_CONFIG',
|
CONFIGURE_DOWNLOAD_ALL_CONFIG: 'CONFIGURE_DOWNLOAD_ALL_CONFIG',
|
||||||
|
CONFIGURE_SYNC_GALLERY_DB: 'CONFIGURE_SYNC_GALLERY_DB',
|
||||||
|
|
||||||
// advanced setting rpc
|
// advanced setting rpc
|
||||||
ADVANCED_UPDATE_SERVER: 'ADVANCED_UPDATE_SERVER',
|
ADVANCED_UPDATE_SERVER: 'ADVANCED_UPDATE_SERVER',
|
||||||
|
|||||||
@@ -7249,7 +7249,7 @@ external-editor@^3.0.3, external-editor@^3.1.0:
|
|||||||
|
|
||||||
extract-zip@^2.0.1:
|
extract-zip@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
|
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
|
||||||
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
|
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "^4.1.1"
|
debug "^4.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user