+import { registerTSPaths } from '../server/helpers/register-ts-paths'
+registerTSPaths()
+
import * as prompt from 'prompt'
import { join } from 'path'
-import { readdirPromise, unlinkPromise } from '../server/helpers/core-utils'
-import { CONFIG } from '../server/initializers/constants'
+import { CONFIG } from '../server/initializers/config'
import { VideoModel } from '../server/models/video/video'
-import { initDatabaseModels } from '../server/initializers'
+import { initDatabaseModels } from '../server/initializers/database'
+import { readdir, remove } from 'fs-extra'
+import { VideoRedundancyModel } from '../server/models/redundancy/video-redundancy'
+import * as Bluebird from 'bluebird'
+import { getUUIDFromFilename } from '../server/helpers/utils'
+import { ThumbnailModel } from '../server/models/video/thumbnail'
+import { AvatarModel } from '../server/models/avatar/avatar'
+import { uniq, values } from 'lodash'
run()
.then(() => process.exit(0))
})
async function run () {
- await initDatabaseModels(true)
+ const dirs = values(CONFIG.STORAGE)
- const storageToPrune = [
- CONFIG.STORAGE.VIDEOS_DIR,
- CONFIG.STORAGE.PREVIEWS_DIR,
- CONFIG.STORAGE.THUMBNAILS_DIR,
- CONFIG.STORAGE.TORRENTS_DIR
- ]
+ if (uniq(dirs).length !== dirs.length) {
+ console.error('Cannot prune storage because you put multiple storage keys in the same directory.')
+ process.exit(0)
+ }
+
+ await initDatabaseModels(true)
let toDelete: string[] = []
- for (const directory of storageToPrune) {
- toDelete = toDelete.concat(await pruneDirectory(directory))
- }
+
+ toDelete = toDelete.concat(
+ await pruneDirectory(CONFIG.STORAGE.VIDEOS_DIR, doesVideoExist(true)),
+ await pruneDirectory(CONFIG.STORAGE.TORRENTS_DIR, doesVideoExist(true)),
+
+ await pruneDirectory(CONFIG.STORAGE.REDUNDANCY_DIR, doesRedundancyExist),
+
+ await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true)),
+ await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false)),
+
+ await pruneDirectory(CONFIG.STORAGE.AVATARS_DIR, doesAvatarExist)
+ )
+
+ const tmpFiles = await readdir(CONFIG.STORAGE.TMP_DIR)
+ toDelete = toDelete.concat(tmpFiles.map(t => join(CONFIG.STORAGE.TMP_DIR, t)))
if (toDelete.length === 0) {
console.log('No files to delete.')
console.log('Processing delete...\n')
for (const path of toDelete) {
- await unlinkPromise(path)
+ await remove(path)
}
console.log('Done!')
}
}
-async function pruneDirectory (directory: string) {
- const files = await readdirPromise(directory)
+type ExistFun = (file: string) => Promise<boolean>
+async function pruneDirectory (directory: string, existFun: ExistFun) {
+ const files = await readdir(directory)
const toDelete: string[] = []
- for (const file of files) {
+ await Bluebird.map(files, async file => {
+ if (await existFun(file) !== true) {
+ toDelete.push(join(directory, file))
+ }
+ }, { concurrency: 20 })
+
+ return toDelete
+}
+
+function doesVideoExist (keepOnlyOwned: boolean) {
+ return async (file: string) => {
const uuid = getUUIDFromFilename(file)
- let video: VideoModel
+ const video = await VideoModel.loadByUUID(uuid)
- if (uuid) video = await VideoModel.loadByUUID(uuid)
+ return video && (keepOnlyOwned === false || video.isOwned())
+ }
+}
+
+function doesThumbnailExist (keepOnlyOwned: boolean) {
+ return async (file: string) => {
+ const thumbnail = await ThumbnailModel.loadByName(file)
+ if (!thumbnail) return false
+
+ if (keepOnlyOwned) {
+ const video = await VideoModel.load(thumbnail.videoId)
+ if (video.isOwned() === false) return false
+ }
- if (!uuid || !video) toDelete.push(join(directory, file))
+ return true
}
+}
- return toDelete
+async function doesAvatarExist (file: string) {
+ const avatar = await AvatarModel.loadByName(file)
+
+ return !!avatar
}
-function getUUIDFromFilename (filename: string) {
- const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
- const result = filename.match(regex)
+async function doesRedundancyExist (file: string) {
+ const uuid = getUUIDFromFilename(file)
+ const video = await VideoModel.loadWithFiles(uuid)
+
+ if (!video) return false
- if (!result || Array.isArray(result) === false) return null
+ const isPlaylist = file.includes('.') === false
- return result[0]
+ if (isPlaylist) {
+ const p = video.getHLSPlaylist()
+ if (!p) return false
+
+ const redundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(p.id)
+ return !!redundancy
+ }
+
+ const resolution = parseInt(file.split('-')[5], 10)
+ if (isNaN(resolution)) {
+ console.error('Cannot prune %s because we cannot guess guess the resolution.', file)
+ return true
+ }
+
+ const videoFile = video.getWebTorrentFile(resolution)
+ if (!videoFile) {
+ console.error('Cannot find webtorrent file of video %s - %d', video.url, resolution)
+ return true
+ }
+
+ const redundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id)
+ return !!redundancy
}
async function askConfirmation () {
properties: {
confirm: {
type: 'string',
- description: 'Are you sure you want to delete these files? Please check carefully',
+ description: 'These following unused files can be deleted, but please check your backups first (bugs happen).' +
+ ' Notice PeerTube must have been stopped when your ran this script.' +
+ ' Can we delete these files?',
default: 'n',
required: true
}
}
prompt.get(schema, function (err, result) {
if (err) return rej(err)
- return res(result.confirm && result.confirm.match(/y/) !== null)
+
+ return res(result.confirm?.match(/y/) !== null)
})
})
}