Add house-keeping script
This commit is contained in:
parent
9b483bcb78
commit
2b189131fa
97
package.json
97
package.json
|
@ -24,65 +24,66 @@
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"e2e:browserstack": "bash ./scripts/e2e/browserstack.sh",
|
"benchmark-server": "tsx --conditions=peertube:tsx ./scripts/benchmark.ts",
|
||||||
"e2e:local": "bash ./scripts/e2e/local.sh",
|
|
||||||
"build": "bash ./scripts/build/index.sh",
|
|
||||||
"build:embed": "bash ./scripts/build/embed.sh",
|
|
||||||
"build:server": "bash ./scripts/build/server.sh",
|
|
||||||
"build:client": "bash ./scripts/build/client.sh",
|
"build:client": "bash ./scripts/build/client.sh",
|
||||||
"build:peertube-runner": "bash ./scripts/build/peertube-runner.sh",
|
"build:embed": "bash ./scripts/build/embed.sh",
|
||||||
"build:peertube-cli": "bash ./scripts/build/peertube-cli.sh",
|
"build:peertube-cli": "bash ./scripts/build/peertube-cli.sh",
|
||||||
|
"build:peertube-runner": "bash ./scripts/build/peertube-runner.sh",
|
||||||
|
"build:server": "bash ./scripts/build/server.sh",
|
||||||
"build:tests": "bash ./scripts/build/tests.sh",
|
"build:tests": "bash ./scripts/build/tests.sh",
|
||||||
|
"build": "bash ./scripts/build/index.sh",
|
||||||
|
"ci": "bash ./scripts/ci.sh",
|
||||||
"clean:client": "bash ./scripts/clean/client/index.sh",
|
"clean:client": "bash ./scripts/clean/client/index.sh",
|
||||||
"clean:server:test": "bash ./scripts/clean/server/test.sh",
|
"clean:server:test": "bash ./scripts/clean/server/test.sh",
|
||||||
"i18n:update": "bash ./scripts/i18n/update.sh",
|
"client-report": "bash ./scripts/client-report.sh",
|
||||||
"dev": "bash ./scripts/dev/index.sh",
|
"client:build-stats": "tsx --conditions=peertube:tsx ./scripts/client-build-stats.ts",
|
||||||
"dev:server": "bash ./scripts/dev/server.sh",
|
"commander": "commander",
|
||||||
"dev:embed": "bash ./scripts/dev/embed.sh",
|
"concurrently": "concurrently",
|
||||||
"dev:client": "bash ./scripts/dev/client.sh",
|
"create-generate-storyboard-job": "node ./dist/scripts/create-generate-storyboard-job.js",
|
||||||
"dev:peertube-cli": "bash ./scripts/dev/peertube-cli.sh",
|
|
||||||
"dev:peertube-runner": "bash ./scripts/dev/peertube-runner.sh",
|
|
||||||
"start": "node dist/server",
|
|
||||||
"start:server": "node dist/server --no-client",
|
|
||||||
"plugin:install": "node ./dist/scripts/plugin/install.js",
|
|
||||||
"plugin:uninstall": "node ./dist/scripts/plugin/uninstall.js",
|
|
||||||
"reset-password": "node ./dist/scripts/reset-password.js",
|
|
||||||
"update-object-storage-url": "LOGGER_LEVEL=warn node ./dist/scripts/update-object-storage-url.js",
|
|
||||||
"update-host": "node ./dist/scripts/update-host.js",
|
|
||||||
"regenerate-thumbnails": "node ./dist/scripts/regenerate-thumbnails.js",
|
|
||||||
"create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js",
|
"create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js",
|
||||||
"create-move-video-storage-job": "node ./dist/scripts/create-move-video-storage-job.js",
|
"create-move-video-storage-job": "node ./dist/scripts/create-move-video-storage-job.js",
|
||||||
"create-generate-storyboard-job": "node ./dist/scripts/create-generate-storyboard-job.js",
|
"dev:client": "bash ./scripts/dev/client.sh",
|
||||||
"parse-log": "node ./dist/scripts/parse-log.js",
|
"dev:embed": "bash ./scripts/dev/embed.sh",
|
||||||
"prune-storage": "LOGGER_LEVEL=warn node ./dist/scripts/prune-storage.js",
|
"dev:peertube-cli": "bash ./scripts/dev/peertube-cli.sh",
|
||||||
"test": "bash ./scripts/test.sh",
|
"dev:peertube-runner": "bash ./scripts/dev/peertube-runner.sh",
|
||||||
"generate-cli-doc": "bash ./scripts/generate-cli-doc.sh",
|
"dev:server": "bash ./scripts/dev/server.sh",
|
||||||
"generate-types-package": "tsx --conditions=peertube:tsx ./packages/types-generator/generate-package.ts",
|
"dev": "bash ./scripts/dev/index.sh",
|
||||||
"i18n:create-custom-files": "tsx --tsconfig ./scripts/tsconfig.json --conditions=peertube:tsx ./scripts/i18n/create-custom-files.ts",
|
"e2e:browserstack": "bash ./scripts/e2e/browserstack.sh",
|
||||||
"benchmark-server": "tsx --conditions=peertube:tsx ./scripts/benchmark.ts",
|
"e2e:local": "bash ./scripts/e2e/local.sh",
|
||||||
"client:build-stats": "tsx --conditions=peertube:tsx ./scripts/client-build-stats.ts",
|
|
||||||
"generate-code-contributors": "tsx --conditions=peertube:tsx ./scripts/generate-code-contributors.ts",
|
|
||||||
"simulate-many-viewers": "tsx --conditions=peertube:tsx ./scripts/simulate-many-viewers.ts",
|
|
||||||
"postinstall": "test -n \"$NOCLIENT\" || (cd client && yarn install --pure-lockfile)",
|
|
||||||
"tsc": "tsc",
|
|
||||||
"commander": "commander",
|
|
||||||
"lint": "npm run ci -- lint",
|
|
||||||
"ng": "ng",
|
|
||||||
"tsx": "tsx",
|
|
||||||
"eslint": "eslint",
|
"eslint": "eslint",
|
||||||
"resolve-tspaths": "resolve-tspaths",
|
"generate-cli-doc": "bash ./scripts/generate-cli-doc.sh",
|
||||||
"resolve-tspaths:server": "npm run resolve-tspaths -- --project server/tsconfig.json --src server --out dist",
|
"generate-code-contributors": "tsx --conditions=peertube:tsx ./scripts/generate-code-contributors.ts",
|
||||||
"resolve-tspaths:server-lib": "npm run resolve-tspaths -- --project server/tsconfig.lib.json --src server --out server/dist",
|
"generate-types-package": "tsx --conditions=peertube:tsx ./packages/types-generator/generate-package.ts",
|
||||||
"resolve-tspaths:tests": "npm run resolve-tspaths -- --project packages/tests/tsconfig.json --src packages/tests/src --out packages/tests/dist",
|
"house-keeping": "LOGGER_LEVEL=warn node ./dist/scripts/house-keeping.js",
|
||||||
"concurrently": "concurrently",
|
"i18n:create-custom-files": "tsx --tsconfig ./scripts/tsconfig.json --conditions=peertube:tsx ./scripts/i18n/create-custom-files.ts",
|
||||||
|
"i18n:update": "bash ./scripts/i18n/update.sh",
|
||||||
|
"lint": "npm run ci -- lint",
|
||||||
"mocha": "mocha",
|
"mocha": "mocha",
|
||||||
"ci": "bash ./scripts/ci.sh",
|
"ng": "ng",
|
||||||
"release": "bash ./scripts/release.sh",
|
|
||||||
"release-embed-api": "bash ./scripts/release-embed-api.sh",
|
|
||||||
"nightly": "bash ./scripts/nightly.sh",
|
"nightly": "bash ./scripts/nightly.sh",
|
||||||
"openapi-clients": "bash ./scripts/openapi-clients.sh",
|
"openapi-clients": "bash ./scripts/openapi-clients.sh",
|
||||||
"client-report": "bash ./scripts/client-report.sh",
|
"parse-log": "node ./dist/scripts/parse-log.js",
|
||||||
"swagger-cli": "swagger-cli"
|
"plugin:install": "node ./dist/scripts/plugin/install.js",
|
||||||
|
"plugin:uninstall": "node ./dist/scripts/plugin/uninstall.js",
|
||||||
|
"postinstall": "test -n \"$NOCLIENT\" || (cd client && yarn install --pure-lockfile)",
|
||||||
|
"prune-storage": "LOGGER_LEVEL=warn node ./dist/scripts/prune-storage.js",
|
||||||
|
"regenerate-thumbnails": "node ./dist/scripts/regenerate-thumbnails.js",
|
||||||
|
"release-embed-api": "bash ./scripts/release-embed-api.sh",
|
||||||
|
"release": "bash ./scripts/release.sh",
|
||||||
|
"reset-password": "node ./dist/scripts/reset-password.js",
|
||||||
|
"resolve-tspaths:server-lib": "npm run resolve-tspaths -- --project server/tsconfig.lib.json --src server --out server/dist",
|
||||||
|
"resolve-tspaths:server": "npm run resolve-tspaths -- --project server/tsconfig.json --src server --out dist",
|
||||||
|
"resolve-tspaths:tests": "npm run resolve-tspaths -- --project packages/tests/tsconfig.json --src packages/tests/src --out packages/tests/dist",
|
||||||
|
"resolve-tspaths": "resolve-tspaths",
|
||||||
|
"simulate-many-viewers": "tsx --conditions=peertube:tsx ./scripts/simulate-many-viewers.ts",
|
||||||
|
"start:server": "node dist/server --no-client",
|
||||||
|
"start": "node dist/server",
|
||||||
|
"swagger-cli": "swagger-cli",
|
||||||
|
"test": "bash ./scripts/test.sh",
|
||||||
|
"tsc": "tsc",
|
||||||
|
"tsx": "tsx",
|
||||||
|
"update-host": "node ./dist/scripts/update-host.js",
|
||||||
|
"update-object-storage-url": "LOGGER_LEVEL=warn node ./dist/scripts/update-object-storage-url.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.190.0",
|
"@aws-sdk/client-s3": "^3.190.0",
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
|
import { arrayify } from '@peertube/peertube-core-utils'
|
||||||
import { PeerTubeServer } from '../server/server.js'
|
import { PeerTubeServer } from '../server/server.js'
|
||||||
|
|
||||||
async function setDefaultAccountAvatar (serversArg: PeerTubeServer | PeerTubeServer[], token?: string) {
|
export async function setDefaultAccountAvatar (serversArg: PeerTubeServer | PeerTubeServer[], token?: string) {
|
||||||
const servers = Array.isArray(serversArg)
|
const servers = arrayify(serversArg)
|
||||||
? serversArg
|
|
||||||
: [ serversArg ]
|
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await server.users.updateMyAvatar({ fixture: 'avatar.png', token })
|
await server.users.updateMyAvatar({ fixture: 'avatar.png', token })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
|
||||||
setDefaultAccountAvatar
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { arrayify } from '@peertube/peertube-core-utils'
|
||||||
import { PeerTubeServer } from '../server/server.js'
|
import { PeerTubeServer } from '../server/server.js'
|
||||||
|
|
||||||
function setDefaultVideoChannel (servers: PeerTubeServer[]) {
|
export function setDefaultVideoChannel (servers: PeerTubeServer[]) {
|
||||||
const tasks: Promise<any>[] = []
|
const tasks: Promise<any>[] = []
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
|
@ -13,17 +14,10 @@ function setDefaultVideoChannel (servers: PeerTubeServer[]) {
|
||||||
return Promise.all(tasks)
|
return Promise.all(tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setDefaultChannelAvatar (serversArg: PeerTubeServer | PeerTubeServer[], channelName: string = 'root_channel') {
|
export async function setDefaultChannelAvatar (serversArg: PeerTubeServer | PeerTubeServer[], channelName: string = 'root_channel') {
|
||||||
const servers = Array.isArray(serversArg)
|
const servers = arrayify(serversArg)
|
||||||
? serversArg
|
|
||||||
: [ serversArg ]
|
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await server.channels.updateImage({ channelName, fixture: 'avatar.png', type: 'avatar' })
|
await server.channels.updateImage({ channelName, fixture: 'avatar.png', type: 'avatar' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
|
||||||
setDefaultVideoChannel,
|
|
||||||
setDefaultChannelAvatar
|
|
||||||
}
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ function listStoryboardFiles (server: PeerTubeServer) {
|
||||||
return readdir(storage)
|
return readdir(storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Test create generate storyboard job', function () {
|
describe('Test create generate storyboard job CLI', function () {
|
||||||
let servers: PeerTubeServer[] = []
|
let servers: PeerTubeServer[] = []
|
||||||
const uuids: string[] = []
|
const uuids: string[] = []
|
||||||
let sql: SQLCommand
|
let sql: SQLCommand
|
||||||
|
|
|
@ -154,7 +154,7 @@ function runTests (enableObjectStorage: boolean) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Test create import video jobs', function () {
|
describe('Test create import video jobs CLI', function () {
|
||||||
|
|
||||||
describe('On filesystem', function () {
|
describe('On filesystem', function () {
|
||||||
runTests(false)
|
runTests(false)
|
||||||
|
|
|
@ -64,7 +64,7 @@ async function checkFiles (origin: PeerTubeServer, video: VideoDetails, objectSt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Test create move video storage job', function () {
|
describe('Test create move video storage job CLI', function () {
|
||||||
if (areMockObjectStorageTestsDisabled()) return
|
if (areMockObjectStorageTestsDisabled()) return
|
||||||
|
|
||||||
let servers: PeerTubeServer[] = []
|
let servers: PeerTubeServer[] = []
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||||
|
import {
|
||||||
|
PeerTubeServer,
|
||||||
|
cleanupTests,
|
||||||
|
createMultipleServers,
|
||||||
|
doubleFollow,
|
||||||
|
makeGetRequest,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
setDefaultAccountAvatar,
|
||||||
|
setDefaultChannelAvatar,
|
||||||
|
waitJobs
|
||||||
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
|
||||||
|
describe('House keeping CLI', function () {
|
||||||
|
let servers: PeerTubeServer[]
|
||||||
|
|
||||||
|
function runHouseKeeping (option: string) {
|
||||||
|
const env = servers[0].cli.getEnv()
|
||||||
|
const command = `echo y | ${env} npm run house-keeping -- ${option}`
|
||||||
|
|
||||||
|
return servers[0].cli.execWithEnv(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRemoteData () {
|
||||||
|
{
|
||||||
|
const { data } = await servers[0].videos.list()
|
||||||
|
for (const video of data) {
|
||||||
|
await makeGetRequest({ url: servers[0].url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
await makeGetRequest({ url: servers[0].url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { data: accounts } = await servers[0].accounts.list()
|
||||||
|
const { data: channels } = await servers[0].channels.list()
|
||||||
|
|
||||||
|
for (const { avatars } of [ ...accounts, ...channels ]) {
|
||||||
|
for (const avatar of avatars) {
|
||||||
|
await makeGetRequest({ url: servers[0].url, path: avatar.path, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(360000)
|
||||||
|
|
||||||
|
servers = await createMultipleServers(2)
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
|
||||||
|
await setDefaultAccountAvatar(servers)
|
||||||
|
await setDefaultChannelAvatar(servers)
|
||||||
|
|
||||||
|
await servers[1].config.enableMinimumTranscoding()
|
||||||
|
|
||||||
|
for (const server of servers) {
|
||||||
|
await server.videos.quickUpload({ name: 'video' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have remote files locally', async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
await fetchRemoteData()
|
||||||
|
|
||||||
|
expect(await servers[0].servers.countFiles('thumbnails')).to.equal(2)
|
||||||
|
expect(await servers[0].servers.countFiles('avatars')).to.equal((2 + 2) * 4) // 2 accounts and 2 channels in 4 versions
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should remove remote files', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await servers[0].kill()
|
||||||
|
await runHouseKeeping('--delete-remote-files')
|
||||||
|
await servers[0].run()
|
||||||
|
|
||||||
|
expect(await servers[0].servers.countFiles('thumbnails')).to.equal(1)
|
||||||
|
expect(await servers[0].servers.countFiles('avatars')).to.equal((1 + 1) * 4) // 1 account and 1 channel in 4 versions
|
||||||
|
|
||||||
|
await fetchRemoteData()
|
||||||
|
|
||||||
|
expect(await servers[0].servers.countFiles('thumbnails')).to.equal(2)
|
||||||
|
expect(await servers[0].servers.countFiles('avatars')).to.equal((2 + 2) * 4) // 2 accounts and 2 channels in 4 versions
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests(servers)
|
||||||
|
})
|
||||||
|
})
|
|
@ -2,6 +2,7 @@
|
||||||
import './create-import-video-file-job'
|
import './create-import-video-file-job'
|
||||||
import './create-generate-storyboard-job'
|
import './create-generate-storyboard-job'
|
||||||
import './create-move-video-storage-job'
|
import './create-move-video-storage-job'
|
||||||
|
import './house-keeping.js'
|
||||||
import './peertube'
|
import './peertube'
|
||||||
import './plugins'
|
import './plugins'
|
||||||
import './prune-storage'
|
import './prune-storage'
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
setAccessTokensToServers
|
setAccessTokensToServers
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
|
|
||||||
describe('Test plugin scripts', function () {
|
describe('Test plugin CLI', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { createFile } from 'fs-extra/esm'
|
||||||
import { readdir } from 'fs/promises'
|
import { readdir } from 'fs/promises'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
describe('Test prune storage scripts', function () {
|
describe('Test prune storage CLI', function () {
|
||||||
let servers: PeerTubeServer[]
|
let servers: PeerTubeServer[]
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
|
|
|
@ -26,7 +26,7 @@ async function testThumbnail (server: PeerTubeServer, videoId: number | string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Test regenerate thumbnails script', function () {
|
describe('Test regenerate thumbnails CLI', function () {
|
||||||
let servers: PeerTubeServer[]
|
let servers: PeerTubeServer[]
|
||||||
|
|
||||||
let video1: Video
|
let video1: Video
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { cleanupTests, CLICommand, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
|
import { cleanupTests, CLICommand, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
|
||||||
|
|
||||||
describe('Test reset password scripts', function () {
|
describe('Test reset password CLI', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
import { parseTorrentVideo } from '@tests/shared/webtorrent.js'
|
import { parseTorrentVideo } from '@tests/shared/webtorrent.js'
|
||||||
import { VideoPlaylistPrivacy } from '@peertube/peertube-models'
|
import { VideoPlaylistPrivacy } from '@peertube/peertube-models'
|
||||||
|
|
||||||
describe('Test update host scripts', function () {
|
describe('Test update host CLI', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
import { expectStartWith } from '@tests/shared/checks.js'
|
import { expectStartWith } from '@tests/shared/checks.js'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
|
|
||||||
describe('Update object storage URL', function () {
|
describe('Update object storage URL CLI', function () {
|
||||||
if (areMockObjectStorageTestsDisabled()) return
|
if (areMockObjectStorageTestsDisabled()) return
|
||||||
|
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
|
|
@ -123,6 +123,5 @@ async function getTorrent (req: express.Request, res: express.Response) {
|
||||||
const result = await VideoTorrentsSimpleFileCache.Instance.getFilePath(req.params.filename)
|
const result = await VideoTorrentsSimpleFileCache.Instance.getFilePath(req.params.filename)
|
||||||
if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
|
if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
|
||||||
|
|
||||||
// Torrents still use the old naming convention (video uuid + .torrent)
|
|
||||||
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })
|
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { getLowercaseExtension } from '@peertube/peertube-node-utils'
|
||||||
import { MActorId, MActorImage, MActorImageFormattable } from '@server/types/models/index.js'
|
import { MActorId, MActorImage, MActorImageFormattable } from '@server/types/models/index.js'
|
||||||
import { remove } from 'fs-extra/esm'
|
import { remove } from 'fs-extra/esm'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import { Op } from 'sequelize'
|
||||||
import {
|
import {
|
||||||
AfterDestroy,
|
AfterDestroy,
|
||||||
AllowNull,
|
AllowNull,
|
||||||
|
@ -76,10 +77,10 @@ export class ActorImageModel extends SequelizeModel<ActorImageModel> {
|
||||||
},
|
},
|
||||||
onDelete: 'CASCADE'
|
onDelete: 'CASCADE'
|
||||||
})
|
})
|
||||||
Actor: Awaited<ActorModel> // Remove awaited: https://github.com/sequelize/sequelize-typescript/issues/825
|
Actor: Awaited<ActorModel> // TODO: Remove awaited: https://github.com/sequelize/sequelize-typescript/issues/825
|
||||||
|
|
||||||
@AfterDestroy
|
@AfterDestroy
|
||||||
static removeFilesAndSendDelete (instance: ActorImageModel) {
|
static removeFile (instance: ActorImageModel) {
|
||||||
logger.info('Removing actor image file %s.', instance.filename)
|
logger.info('Removing actor image file %s.', instance.filename)
|
||||||
|
|
||||||
// Don't block the transaction
|
// Don't block the transaction
|
||||||
|
@ -128,12 +129,34 @@ export class ActorImageModel extends SequelizeModel<ActorImageModel> {
|
||||||
return { avatars, banners }
|
return { avatars, banners }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static listRemoteOnDisk () {
|
||||||
|
return this.findAll<MActorImage>({
|
||||||
|
where: {
|
||||||
|
onDisk: true
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
attributes: [ 'id' ],
|
||||||
|
model: ActorModel.unscoped(),
|
||||||
|
required: true,
|
||||||
|
where: {
|
||||||
|
serverId: {
|
||||||
|
[Op.ne]: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
static getImageUrl (image: MActorImage) {
|
static getImageUrl (image: MActorImage) {
|
||||||
if (!image) return undefined
|
if (!image) return undefined
|
||||||
|
|
||||||
return WEBSERVER.URL + image.getStaticPath()
|
return WEBSERVER.URL + image.getStaticPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
toFormattedJSON (this: MActorImageFormattable): ActorImage {
|
toFormattedJSON (this: MActorImageFormattable): ActorImage {
|
||||||
return {
|
return {
|
||||||
width: this.width,
|
width: this.width,
|
||||||
|
|
|
@ -160,12 +160,34 @@ export class ThumbnailModel extends SequelizeModel<ThumbnailModel> {
|
||||||
return ThumbnailModel.findOne(query)
|
return ThumbnailModel.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static listRemoteOnDisk () {
|
||||||
|
return this.findAll<MThumbnail>({
|
||||||
|
where: {
|
||||||
|
onDisk: true
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
attributes: [ 'id' ],
|
||||||
|
model: VideoModel.unscoped(),
|
||||||
|
required: true,
|
||||||
|
where: {
|
||||||
|
remote: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
static buildPath (type: ThumbnailType_Type, filename: string) {
|
static buildPath (type: ThumbnailType_Type, filename: string) {
|
||||||
const directory = ThumbnailModel.types[type].directory
|
const directory = ThumbnailModel.types[type].directory
|
||||||
|
|
||||||
return join(directory, filename)
|
return join(directory, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
getOriginFileUrl (videoOrPlaylist: MVideo | MVideoPlaylist) {
|
getOriginFileUrl (videoOrPlaylist: MVideo | MVideoPlaylist) {
|
||||||
const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
|
const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { createCommand } from '@commander-js/extra-typings'
|
||||||
|
import { initDatabaseModels } from '@server/initializers/database.js'
|
||||||
|
import { ActorImageModel } from '@server/models/actor/actor-image.js'
|
||||||
|
import { ThumbnailModel } from '@server/models/video/thumbnail.js'
|
||||||
|
import { askConfirmation, displayPeerTubeMustBeStoppedWarning } from './shared/common.js'
|
||||||
|
|
||||||
|
const program = createCommand()
|
||||||
|
.description('Remove unused objects from database or remote files')
|
||||||
|
.option('--delete-remote-files', 'Remove remote files (avatars, banners, thumbnails...)')
|
||||||
|
.parse(process.argv)
|
||||||
|
|
||||||
|
const options = program.opts()
|
||||||
|
|
||||||
|
if (!options.deleteRemoteFiles) {
|
||||||
|
console.log('At least one option must be set (for example --delete-remote-files).')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(-1)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function run () {
|
||||||
|
await initDatabaseModels(true)
|
||||||
|
|
||||||
|
displayPeerTubeMustBeStoppedWarning()
|
||||||
|
|
||||||
|
if (options.deleteRemoteFiles) {
|
||||||
|
return deleteRemoteFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRemoteFiles () {
|
||||||
|
console.log('Detecting remote files that can be deleted...')
|
||||||
|
|
||||||
|
const thumbnails = await ThumbnailModel.listRemoteOnDisk()
|
||||||
|
const actorImages = await ActorImageModel.listRemoteOnDisk()
|
||||||
|
|
||||||
|
if (thumbnails.length === 0 && actorImages.length === 0) {
|
||||||
|
console.log('No remote files to delete detected.')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await askConfirmation(
|
||||||
|
`${thumbnails.length} thumbnails and ${actorImages.length} avatars/banners can be locally deleted. ` +
|
||||||
|
`PeerTube will download them again on-demand.` +
|
||||||
|
`Do you want to delete these remote files?`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res !== true) {
|
||||||
|
console.log('Exiting without delete remote files.')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
console.log('Deleting remote thumbnails...')
|
||||||
|
|
||||||
|
for (const thumbnail of thumbnails) {
|
||||||
|
if (!thumbnail.fileUrl) {
|
||||||
|
console.log(`Skipping thumbnail removal of ${thumbnail.getPath()} as we don't have its remote file URL in the database.`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await thumbnail.removeThumbnail()
|
||||||
|
|
||||||
|
thumbnail.onDisk = false
|
||||||
|
await thumbnail.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
console.log('Deleting remote avatars/banners...')
|
||||||
|
|
||||||
|
for (const actorImage of actorImages) {
|
||||||
|
if (!actorImage.fileUrl) {
|
||||||
|
console.log(`Skipping avatar/banner removal of ${actorImage.getPath()} as we don't have its remote file URL in the database.`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await actorImage.removeImage()
|
||||||
|
|
||||||
|
actorImage.onDisk = false
|
||||||
|
await actorImage.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Remote files deleted!')
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ import Bluebird from 'bluebird'
|
||||||
import { remove } from 'fs-extra/esm'
|
import { remove } from 'fs-extra/esm'
|
||||||
import { readdir, stat } from 'fs/promises'
|
import { readdir, stat } from 'fs/promises'
|
||||||
import { basename, dirname, join } from 'path'
|
import { basename, dirname, join } from 'path'
|
||||||
import prompt from 'prompt'
|
|
||||||
import { getUUIDFromFilename } from '../core/helpers/utils.js'
|
import { getUUIDFromFilename } from '../core/helpers/utils.js'
|
||||||
import { CONFIG } from '../core/initializers/config.js'
|
import { CONFIG } from '../core/initializers/config.js'
|
||||||
import { initDatabaseModels } from '../core/initializers/database.js'
|
import { initDatabaseModels } from '../core/initializers/database.js'
|
||||||
|
@ -18,6 +17,7 @@ import { ActorImageModel } from '../core/models/actor/actor-image.js'
|
||||||
import { VideoRedundancyModel } from '../core/models/redundancy/video-redundancy.js'
|
import { VideoRedundancyModel } from '../core/models/redundancy/video-redundancy.js'
|
||||||
import { ThumbnailModel } from '../core/models/video/thumbnail.js'
|
import { ThumbnailModel } from '../core/models/video/thumbnail.js'
|
||||||
import { VideoModel } from '../core/models/video/video.js'
|
import { VideoModel } from '../core/models/video/video.js'
|
||||||
|
import { askConfirmation, displayPeerTubeMustBeStoppedWarning } from './shared/common.js'
|
||||||
|
|
||||||
run()
|
run()
|
||||||
.then(() => process.exit(0))
|
.then(() => process.exit(0))
|
||||||
|
@ -29,6 +29,8 @@ run()
|
||||||
async function run () {
|
async function run () {
|
||||||
await initDatabaseModels(true)
|
await initDatabaseModels(true)
|
||||||
|
|
||||||
|
displayPeerTubeMustBeStoppedWarning()
|
||||||
|
|
||||||
await new FSPruner().prune()
|
await new FSPruner().prune()
|
||||||
|
|
||||||
console.log('\n')
|
console.log('\n')
|
||||||
|
@ -61,7 +63,7 @@ class ObjectStoragePruner {
|
||||||
const formattedKeysToDelete = this.keysToDelete.map(({ bucket, key }) => ` In bucket ${bucket}: ${key}`).join('\n')
|
const formattedKeysToDelete = this.keysToDelete.map(({ bucket, key }) => ` In bucket ${bucket}: ${key}`).join('\n')
|
||||||
console.log(`${this.keysToDelete.length} unknown files from object storage can be deleted:\n${formattedKeysToDelete}\n`)
|
console.log(`${this.keysToDelete.length} unknown files from object storage can be deleted:\n${formattedKeysToDelete}\n`)
|
||||||
|
|
||||||
const res = await askConfirmation()
|
const res = await askPruneConfirmation()
|
||||||
if (res !== true) {
|
if (res !== true) {
|
||||||
console.log('Exiting without deleting object storage files.')
|
console.log('Exiting without deleting object storage files.')
|
||||||
return
|
return
|
||||||
|
@ -183,7 +185,7 @@ class FSPruner {
|
||||||
const formattedKeysToDelete = this.pathsToDelete.map(p => ` ${p}`).join('\n')
|
const formattedKeysToDelete = this.pathsToDelete.map(p => ` ${p}`).join('\n')
|
||||||
console.log(`${this.pathsToDelete.length} unknown files from filesystem can be deleted:\n${formattedKeysToDelete}\n`)
|
console.log(`${this.pathsToDelete.length} unknown files from filesystem can be deleted:\n${formattedKeysToDelete}\n`)
|
||||||
|
|
||||||
const res = await askConfirmation()
|
const res = await askPruneConfirmation()
|
||||||
if (res !== true) {
|
if (res !== true) {
|
||||||
console.log('Exiting without deleting filesystem files.')
|
console.log('Exiting without deleting filesystem files.')
|
||||||
return
|
return
|
||||||
|
@ -299,29 +301,9 @@ class FSPruner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askConfirmation () {
|
async function askPruneConfirmation () {
|
||||||
return new Promise((res, rej) => {
|
return askConfirmation(
|
||||||
prompt.start()
|
'These unknown files can be deleted, but please check your backups first (bugs happen). ' +
|
||||||
|
'Can we delete these files?'
|
||||||
const schema = {
|
)
|
||||||
properties: {
|
|
||||||
confirm: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'These unknown 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? (y/n)',
|
|
||||||
default: 'n',
|
|
||||||
validator: /y[es]*|n[o]?/,
|
|
||||||
warning: 'Must respond yes or no',
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt.get(schema, function (err, result) {
|
|
||||||
if (err) return rej(err)
|
|
||||||
|
|
||||||
return res(result.confirm?.match(/y/) !== null)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import prompt from 'prompt'
|
||||||
|
|
||||||
|
export async function askConfirmation (message: string) {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
prompt.start()
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
properties: {
|
||||||
|
confirm: {
|
||||||
|
type: 'string',
|
||||||
|
description: message + ' (y/n)',
|
||||||
|
default: 'n',
|
||||||
|
validator: /y[es]*|n[o]?/,
|
||||||
|
warning: 'Must respond yes or no',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt.get(schema, function (err, result) {
|
||||||
|
if (err) return rej(err)
|
||||||
|
|
||||||
|
return res(result.confirm?.match(/y/) !== null)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function displayPeerTubeMustBeStoppedWarning () {
|
||||||
|
console.log(`/!\\ PeerTube must be stopped before running this script /!\\\n`)
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import { FileStorage } from '@peertube/peertube-models'
|
||||||
import { escapeForRegex } from '@server/helpers/regexp.js'
|
import { escapeForRegex } from '@server/helpers/regexp.js'
|
||||||
import { initDatabaseModels, sequelizeTypescript } from '@server/initializers/database.js'
|
import { initDatabaseModels, sequelizeTypescript } from '@server/initializers/database.js'
|
||||||
import { QueryTypes } from 'sequelize'
|
import { QueryTypes } from 'sequelize'
|
||||||
import prompt from 'prompt'
|
import { askConfirmation, displayPeerTubeMustBeStoppedWarning } from './shared/common.js'
|
||||||
|
|
||||||
const program = createCommand()
|
const program = createCommand()
|
||||||
.description('Update PeerTube object file URLs after an object storage migration.')
|
.description('Update PeerTube object file URLs after an object storage migration.')
|
||||||
|
@ -24,6 +24,8 @@ run()
|
||||||
async function run () {
|
async function run () {
|
||||||
await initDatabaseModels(true)
|
await initDatabaseModels(true)
|
||||||
|
|
||||||
|
displayPeerTubeMustBeStoppedWarning()
|
||||||
|
|
||||||
const fromRegexp = `^${escapeForRegex(options.from)}`
|
const fromRegexp = `^${escapeForRegex(options.from)}`
|
||||||
const to = options.to
|
const to = options.to
|
||||||
|
|
||||||
|
@ -58,7 +60,7 @@ async function run () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await askConfirmation()
|
const res = await askUpdateConfirmation()
|
||||||
if (res !== true) {
|
if (res !== true) {
|
||||||
console.log('Exiting without updating URLs.')
|
console.log('Exiting without updating URLs.')
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
|
@ -90,29 +92,9 @@ function parseUrl (value: string) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askConfirmation () {
|
async function askUpdateConfirmation () {
|
||||||
return new Promise((res, rej) => {
|
return askConfirmation(
|
||||||
prompt.start()
|
'These URLs can be updated, but please check your backups first (bugs happen). ' +
|
||||||
|
'Can we update these URLs?'
|
||||||
const schema = {
|
)
|
||||||
properties: {
|
|
||||||
confirm: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'These URLs can be updated, but please check your backups first (bugs happen).' +
|
|
||||||
' Notice PeerTube must have been stopped when your ran this script.' +
|
|
||||||
' Can we update these URLs? (y/n)',
|
|
||||||
default: 'n',
|
|
||||||
validator: /y[es]*|n[o]?/,
|
|
||||||
warning: 'Must respond yes or no',
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt.get(schema, function (err, result) {
|
|
||||||
if (err) return rej(err)
|
|
||||||
|
|
||||||
return res(result.confirm?.match(/y/) !== null)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -494,6 +494,25 @@ docker compose exec -u peertube peertube npm run update-object-storage-url -- --
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
### Cleanup remote files
|
||||||
|
|
||||||
|
**PeerTube >= 6.2**
|
||||||
|
|
||||||
|
Use this script to recover disk space by removing remote files (thumbnails, avatars...) that can be re-fetched later by your PeerTube instance on-demand:
|
||||||
|
|
||||||
|
```bash [Classic installation]
|
||||||
|
cd /var/www/peertube/peertube-latest
|
||||||
|
sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run house-keeping -- --delete-remote-files
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash [Docker]
|
||||||
|
cd /var/www/peertube-docker
|
||||||
|
docker compose exec -u peertube peertube npm run house-keeping -- --delete-remote-files
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
### Generate storyboard
|
### Generate storyboard
|
||||||
|
|
||||||
**PeerTube >= 6.0**
|
**PeerTube >= 6.0**
|
||||||
|
|
Loading…
Reference in New Issue