backup.service.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import { BrowserWindow } from 'electron'
  2. import * as fs from 'fs'
  3. import * as path from 'path'
  4. import { IpcChannels } from '@shared/ipc-channels'
  5. import type { WallpaperMeta } from '@shared/types'
  6. import { getConfiguredBackupPath } from './config.service'
  7. import * as library from './library.service'
  8. import { readProjectJson, normalizeType, normalizeRating, getDirSize } from './library.service'
  9. export function getBackupDir(id: string): string {
  10. const backupPath = getConfiguredBackupPath()
  11. if (!backupPath) throw new Error('Backup path must be configured before backing up')
  12. return path.join(backupPath, id)
  13. }
  14. export function isBackedUp(id: string): boolean {
  15. try {
  16. return fs.existsSync(getBackupDir(id))
  17. } catch {
  18. return false
  19. }
  20. }
  21. function walkFiles(dirPath: string, baseDir = dirPath): { relPath: string; size: number }[] {
  22. const results: { relPath: string; size: number }[] = []
  23. for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
  24. const full = path.join(dirPath, entry.name)
  25. if (entry.isDirectory()) {
  26. results.push(...walkFiles(full, baseDir))
  27. } else if (entry.isFile()) {
  28. const rel = path.relative(baseDir, full)
  29. results.push({ relPath: rel, size: fs.statSync(full).size })
  30. }
  31. }
  32. return results
  33. }
  34. export async function backupWallpaper(
  35. id: string,
  36. sourcePath: string,
  37. win: BrowserWindow
  38. ): Promise<void> {
  39. const destDir = getBackupDir(id)
  40. const files = walkFiles(sourcePath)
  41. const bytesTotal = files.reduce((sum, f) => sum + f.size, 0)
  42. let bytesCopied = 0
  43. fs.mkdirSync(destDir, { recursive: true })
  44. try {
  45. for (const file of files) {
  46. const srcFile = path.join(sourcePath, file.relPath)
  47. const destFile = path.join(destDir, file.relPath)
  48. fs.mkdirSync(path.dirname(destFile), { recursive: true })
  49. await fs.promises.copyFile(srcFile, destFile)
  50. bytesCopied += file.size
  51. if (!win.isDestroyed()) {
  52. win.webContents.send(IpcChannels.EVENT_BACKUP_PROGRESS, {
  53. itemId: id,
  54. bytesCopied,
  55. bytesTotal,
  56. percentage: bytesTotal > 0 ? Math.round((bytesCopied / bytesTotal) * 100) : 100,
  57. status: 'copying'
  58. })
  59. }
  60. }
  61. } catch (err) {
  62. if (!win.isDestroyed()) {
  63. win.webContents.send(IpcChannels.EVENT_BACKUP_PROGRESS, {
  64. itemId: id,
  65. bytesCopied,
  66. bytesTotal,
  67. percentage: 0,
  68. status: 'error',
  69. message: (err as Error).message
  70. })
  71. }
  72. throw err
  73. }
  74. if (!win.isDestroyed()) {
  75. win.webContents.send(IpcChannels.EVENT_BACKUP_PROGRESS, {
  76. itemId: id,
  77. bytesCopied,
  78. bytesTotal,
  79. percentage: 100,
  80. status: 'completed'
  81. })
  82. }
  83. }
  84. function buildBackupMeta(
  85. id: string,
  86. localPath: string,
  87. existing?: WallpaperMeta
  88. ): WallpaperMeta {
  89. const pj = readProjectJson(localPath)
  90. const now = Date.now()
  91. return {
  92. id,
  93. title: pj?.title ?? existing?.title ?? `Wallpaper ${id}`,
  94. type: normalizeType(pj?.type),
  95. contentRating: normalizeRating(pj?.contentrating),
  96. description: pj?.description ?? existing?.description,
  97. previewLocal: pj?.preview ? path.join(localPath, pj.preview) : undefined,
  98. localPath,
  99. file: pj?.file,
  100. fileSize: getDirSize(localPath),
  101. createdAt: existing?.createdAt ?? now,
  102. updatedAt: now,
  103. subscribed: false,
  104. appliedCount: existing?.appliedCount ?? 0,
  105. source: 'backup',
  106. tags: pj?.tags ?? existing?.tags ?? [],
  107. categories: existing?.categories ?? [],
  108. backedUp: true
  109. }
  110. }
  111. export function scanBackupFolder(): { imported: number; skipped: number; linked: number } {
  112. const backupPath = getConfiguredBackupPath()
  113. if (!backupPath || !fs.existsSync(backupPath)) return { imported: 0, skipped: 0, linked: 0 }
  114. let imported = 0
  115. let skipped = 0
  116. let linked = 0
  117. for (const entry of fs.readdirSync(backupPath, { withFileTypes: true })) {
  118. if (!entry.isDirectory()) continue
  119. const id = entry.name
  120. const localBackupPath = path.join(backupPath, id)
  121. const existing = library.getWallpaper(id)
  122. if (existing && existing.source === 'workshop') {
  123. if (!existing.backedUp) {
  124. library.updateWallpaper(id, { backedUp: true })
  125. linked++
  126. } else {
  127. skipped++
  128. }
  129. continue
  130. }
  131. const meta = buildBackupMeta(id, localBackupPath, existing ?? undefined)
  132. library.upsertWallpaper(meta)
  133. imported++
  134. }
  135. return { imported, skipped, linked }
  136. }