prune-storage.ts 5.8 KB

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