prune-storage.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { registerTSPaths } from '../server/helpers/register-ts-paths'
  2. registerTSPaths()
  3. import { start, get } from 'prompt'
  4. import { join, basename } from 'path'
  5. import { CONFIG } from '../server/initializers/config'
  6. import { VideoModel } from '../server/models/video/video'
  7. import { initDatabaseModels } from '../server/initializers/database'
  8. import { readdir, remove, stat } from 'fs-extra'
  9. import { VideoRedundancyModel } from '../server/models/redundancy/video-redundancy'
  10. import { map } from 'bluebird'
  11. import { getUUIDFromFilename } from '../server/helpers/utils'
  12. import { ThumbnailModel } from '../server/models/video/thumbnail'
  13. import { ActorImageModel } from '../server/models/actor/actor-image'
  14. import { uniq, values } from 'lodash'
  15. import { ThumbnailType } from '@shared/models'
  16. import { VideoFileModel } from '@server/models/video/video-file'
  17. run()
  18. .then(() => process.exit(0))
  19. .catch(err => {
  20. console.error(err)
  21. process.exit(-1)
  22. })
  23. async function run () {
  24. const dirs = values(CONFIG.STORAGE)
  25. if (uniq(dirs).length !== dirs.length) {
  26. console.error('Cannot prune storage because you put multiple storage keys in the same directory.')
  27. process.exit(0)
  28. }
  29. await initDatabaseModels(true)
  30. let toDelete: string[] = []
  31. console.log('Detecting files to remove, it could take a while...')
  32. toDelete = toDelete.concat(
  33. await pruneDirectory(CONFIG.STORAGE.VIDEOS_DIR, doesWebTorrentFileExist()),
  34. await pruneDirectory(CONFIG.STORAGE.TORRENTS_DIR, doesTorrentFileExist()),
  35. await pruneDirectory(CONFIG.STORAGE.REDUNDANCY_DIR, doesRedundancyExist),
  36. await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true, ThumbnailType.PREVIEW)),
  37. await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false, ThumbnailType.MINIATURE)),
  38. await pruneDirectory(CONFIG.STORAGE.ACTOR_IMAGES, doesActorImageExist)
  39. )
  40. const tmpFiles = await readdir(CONFIG.STORAGE.TMP_DIR)
  41. toDelete = toDelete.concat(tmpFiles.map(t => join(CONFIG.STORAGE.TMP_DIR, t)))
  42. if (toDelete.length === 0) {
  43. console.log('No files to delete.')
  44. return
  45. }
  46. console.log('Will delete %d files:\n\n%s\n\n', toDelete.length, toDelete.join('\n'))
  47. const res = await askConfirmation()
  48. if (res === true) {
  49. console.log('Processing delete...\n')
  50. for (const path of toDelete) {
  51. await remove(path)
  52. }
  53. console.log('Done!')
  54. } else {
  55. console.log('Exiting without deleting files.')
  56. }
  57. }
  58. type ExistFun = (file: string) => Promise<boolean>
  59. async function pruneDirectory (directory: string, existFun: ExistFun) {
  60. const files = await readdir(directory)
  61. const toDelete: string[] = []
  62. await map(files, async file => {
  63. const filePath = join(directory, file)
  64. if (await existFun(filePath) !== true) {
  65. toDelete.push(filePath)
  66. }
  67. }, { concurrency: 20 })
  68. return toDelete
  69. }
  70. function doesWebTorrentFileExist () {
  71. return (filePath: string) => VideoFileModel.doesOwnedWebTorrentVideoFileExist(basename(filePath))
  72. }
  73. function doesTorrentFileExist () {
  74. return (filePath: string) => VideoFileModel.doesOwnedTorrentFileExist(basename(filePath))
  75. }
  76. function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) {
  77. return async (filePath: string) => {
  78. const thumbnail = await ThumbnailModel.loadByFilename(basename(filePath), type)
  79. if (!thumbnail) return false
  80. if (keepOnlyOwned) {
  81. const video = await VideoModel.load(thumbnail.videoId)
  82. if (video.isOwned() === false) return false
  83. }
  84. return true
  85. }
  86. }
  87. async function doesActorImageExist (filePath: string) {
  88. const image = await ActorImageModel.loadByName(basename(filePath))
  89. return !!image
  90. }
  91. async function doesRedundancyExist (filePath: string) {
  92. const isPlaylist = (await stat(filePath)).isDirectory()
  93. if (isPlaylist) {
  94. const uuid = getUUIDFromFilename(filePath)
  95. const video = await VideoModel.loadWithFiles(uuid)
  96. if (!video) return false
  97. const p = video.getHLSPlaylist()
  98. if (!p) return false
  99. const redundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(p.id)
  100. return !!redundancy
  101. }
  102. const file = await VideoFileModel.loadByFilename(basename(filePath))
  103. if (!file) return false
  104. const redundancy = await VideoRedundancyModel.loadLocalByFileId(file.id)
  105. return !!redundancy
  106. }
  107. async function askConfirmation () {
  108. return new Promise((res, rej) => {
  109. start()
  110. const schema = {
  111. properties: {
  112. confirm: {
  113. type: 'string',
  114. description: 'These following unused files can be deleted, but please check your backups first (bugs happen).' +
  115. ' Notice PeerTube must have been stopped when your ran this script.' +
  116. ' Can we delete these files?',
  117. default: 'n',
  118. required: true
  119. }
  120. }
  121. }
  122. get(schema, function (err, result) {
  123. if (err) return rej(err)
  124. return res(result.confirm?.match(/y/) !== null)
  125. })
  126. })
  127. }