+import { registerTSPaths } from '../server/helpers/register-ts-paths'
+registerTSPaths()
+
import * as prompt from 'prompt'
import { join } from 'path'
-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 { remove, readdir } from 'fs-extra'
+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 storageOnlyOwnedToPrune = [
- CONFIG.STORAGE.VIDEOS_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)
+ }
- const storageForAllToPrune = [
- CONFIG.STORAGE.PREVIEWS_DIR,
- CONFIG.STORAGE.THUMBNAILS_DIR
- ]
+ await initDatabaseModels(true)
let toDelete: string[] = []
- for (const directory of storageOnlyOwnedToPrune) {
- toDelete = toDelete.concat(await pruneDirectory(directory, true))
- }
- for (const directory of storageForAllToPrune) {
- toDelete = toDelete.concat(await pruneDirectory(directory, false))
- }
+ 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.')
}
}
-async function pruneDirectory (directory: string, onlyOwned = false) {
+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
- let localRedundancy: boolean
+ const video = await VideoModel.loadByUUID(uuid)
- if (uuid) {
- video = await VideoModel.loadByUUIDWithFile(uuid)
- localRedundancy = await VideoRedundancyModel.isLocalByVideoUUIDExists(uuid)
- }
+ return video && (keepOnlyOwned === false || video.isOwned())
+ }
+}
- if (
- !uuid ||
- !video ||
- (onlyOwned === true && (video.isOwned() === false && localRedundancy === false))
- ) {
- toDelete.push(join(directory, file))
+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
}
+
+ return true
}
+}
- return toDelete
+async function doesAvatarExist (file: string) {
+ const avatar = await AvatarModel.loadByName(file)
+
+ return !!avatar
+}
+
+async function doesRedundancyExist (file: string) {
+ const uuid = getUUIDFromFilename(file)
+ const video = await VideoModel.loadWithFiles(uuid)
+
+ if (!video) return false
+
+ const isPlaylist = file.includes('.') === false
+
+ 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 () {
confirm: {
type: 'string',
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)
})
})
}