prune-storage.ts 5.3 KB

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