Add config option to keep original video file (basic first version) (#6157)

* testing not removing old file and adding columb to db

* implement feature

* remove unnecessary config changes

* use only keptOriginalFileName, change keptOriginalFileName to keptOriginalFilename for consistency with with videoFile table, slight refactor with basename()

* save original video files to dedicated directory original-video-files

* begin implementing object storage (bucket) support

---------

Co-authored-by: chagai.friedlander <chagai.friedlander@fairkom.eu>
Co-authored-by: Ian <ian.kraft@hotmail.com>
Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
chagai95
2024-03-15 15:47:18 +01:00
committed by GitHub
parent ae31e90c30
commit e57c3024f4
75 changed files with 1653 additions and 801 deletions

View File

@@ -1,5 +1,6 @@
import {
LiveVideoLatencyModeType,
VideoFileMetadata,
VideoPrivacyType,
VideoStateType,
VideoStreamingPlaylistType_Type
@@ -85,7 +86,17 @@ export interface VideoExportJSON {
}[]
source?: {
filename: string
inputFilename: string
resolution: number
size: number
width: number
height: number
fps: number
metadata: VideoFileMetadata
}
archiveFiles: {

View File

@@ -117,6 +117,10 @@ export interface CustomConfig {
transcoding: {
enabled: boolean
originalFile: {
keep: boolean
}
allowAdditionalExtensions: boolean
allowAudioFiles: boolean

View File

@@ -1,4 +1,23 @@
import { VideoFileMetadata } from './file/index.js'
import { VideoConstant } from './video-constant.model.js'
export interface VideoSource {
filename: string
inputFilename: string
resolution?: VideoConstant<number>
size?: number // Bytes
width?: number
height?: number
fileDownloadUrl: string
fps?: number
metadata?: VideoFileMetadata
createdAt: string | Date
// TODO: remove, deprecated in 6.1
filename: string
}

View File

@@ -106,6 +106,19 @@ export class ConfigCommand extends AbstractCommand {
// ---------------------------------------------------------------------------
keepSourceFile () {
return this.updateExistingSubConfig({
newConfig: {
transcoding: {
originalFile: {
keep: true
}
}
}
})
}
// ---------------------------------------------------------------------------
enableChannelSync () {
return this.setChannelSyncEnabled(true)
}
@@ -234,13 +247,17 @@ export class ConfigCommand extends AbstractCommand {
webVideo?: boolean // default true
hls?: boolean // default true
with0p?: boolean // default false
keepOriginal?: boolean // default false
} = {}) {
const { webVideo = true, hls = true, with0p = false } = options
const { webVideo = true, hls = true, with0p = false, keepOriginal = false } = options
return this.updateExistingSubConfig({
newConfig: {
transcoding: {
enabled: true,
originalFile: {
keep: keepOriginal
},
allowAudioFiles: true,
allowAdditionalExtensions: true,
@@ -261,13 +278,17 @@ export class ConfigCommand extends AbstractCommand {
enableMinimumTranscoding (options: {
webVideo?: boolean // default true
hls?: boolean // default true
keepOriginal?: boolean // default false
} = {}) {
const { webVideo = true, hls = true } = options
const { webVideo = true, hls = true, keepOriginal = false } = options
return this.updateExistingSubConfig({
newConfig: {
transcoding: {
enabled: true,
originalFile: {
keep: keepOriginal
},
allowAudioFiles: true,
allowAdditionalExtensions: true,
@@ -560,6 +581,9 @@ export class ConfigCommand extends AbstractCommand {
},
transcoding: {
enabled: true,
originalFile: {
keep: false
},
remoteRunners: {
enabled: false
},

View File

@@ -1,5 +1,5 @@
import { randomInt } from 'crypto'
import { HttpStatusCode } from '@peertube/peertube-models'
import { randomInt } from 'crypto'
import { makePostBodyRequest } from '../requests/index.js'
export class ObjectStorageCommand {
@@ -50,6 +50,14 @@ export class ObjectStorageCommand {
web_videos: {
bucket_name: this.getMockWebVideosBucketName()
},
user_exports: {
bucket_name: this.getMockUserExportBucketName()
},
original_video_files: {
bucket_name: this.getMockOriginalFileBucketName()
}
}
}
@@ -63,6 +71,14 @@ export class ObjectStorageCommand {
return `http://${this.getMockStreamingPlaylistsBucketName()}.${ObjectStorageCommand.getMockEndpointHost()}/`
}
getMockUserExportBaseUrl () {
return `http://${this.getMockUserExportBucketName()}.${ObjectStorageCommand.getMockEndpointHost()}/`
}
getMockOriginalFileBaseUrl () {
return `http://${this.getMockOriginalFileBucketName()}.${ObjectStorageCommand.getMockEndpointHost()}/`
}
async prepareDefaultMockBuckets () {
await this.createMockBucket(this.getMockStreamingPlaylistsBucketName())
await this.createMockBucket(this.getMockWebVideosBucketName())
@@ -100,6 +116,14 @@ export class ObjectStorageCommand {
return this.getMockBucketName(name)
}
getMockUserExportBucketName (name = 'user-exports') {
return this.getMockBucketName(name)
}
getMockOriginalFileBucketName (name = 'original-video-files') {
return this.getMockBucketName(name)
}
getMockBucketName (name: string) {
return `${this.seed}-${name}`
}

View File

@@ -379,6 +379,7 @@ export class PeerTubeServer {
avatars: this.getDirectoryPath('avatars') + '/',
web_videos: this.getDirectoryPath('web-videos') + '/',
streaming_playlists: this.getDirectoryPath('streaming-playlists') + '/',
original_video_files: this.getDirectoryPath('original-video-files') + '/',
redundancy: this.getDirectoryPath('redundancy') + '/',
logs: this.getDirectoryPath('logs') + '/',
previews: this.getDirectoryPath('previews') + '/',

View File

@@ -1,10 +1,10 @@
import { exec } from 'child_process'
import { copy, ensureDir, remove } from 'fs-extra/esm'
import { readdir, readFile } from 'fs/promises'
import { basename, join } from 'path'
import { wait } from '@peertube/peertube-core-utils'
import { HttpStatusCode } from '@peertube/peertube-models'
import { getFileSize, isGithubCI, root } from '@peertube/peertube-node-utils'
import { isGithubCI, root } from '@peertube/peertube-node-utils'
import { exec } from 'child_process'
import { copy, ensureDir, remove } from 'fs-extra/esm'
import { readFile, readdir } from 'fs/promises'
import { basename, join } from 'path'
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
export class ServersCommand extends AbstractCommand {
@@ -84,6 +84,8 @@ export class ServersCommand extends AbstractCommand {
return files.length
}
// ---------------------------------------------------------------------------
buildWebVideoFilePath (fileUrl: string) {
return this.buildDirectory(join('web-videos', basename(fileUrl)))
}
@@ -92,13 +94,9 @@ export class ServersCommand extends AbstractCommand {
return this.buildDirectory(join('streaming-playlists', 'hls', videoUUID, basename(fileUrl)))
}
// ---------------------------------------------------------------------------
getLogContent () {
return readFile(this.buildDirectory('logs/peertube.log'))
}
async getServerFileSize (subPath: string) {
const path = this.server.servers.buildDirectory(subPath)
return getFileSize(path)
}
}

View File

@@ -1,7 +1,8 @@
import { HttpStatusCode, ResultList, UserExport, UserExportRequestResult, UserExportState } from '@peertube/peertube-models'
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
import { wait } from '@peertube/peertube-core-utils'
import { unwrapBody } from '../requests/requests.js'
import { HttpStatusCode, ResultList, UserExport, UserExportRequestResult, UserExportState } from '@peertube/peertube-models'
import { writeFile } from 'fs/promises'
import { makeRawRequest, unwrapBody } from '../requests/requests.js'
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
export class UserExportsCommand extends AbstractCommand {
@@ -49,6 +50,22 @@ export class UserExportsCommand extends AbstractCommand {
})
}
async downloadLatestArchive (options: OverrideCommandOptions & {
userId: number
destination: string
}) {
const { data } = await this.list(options)
const res = await makeRawRequest({
url: data[0].privateDownloadUrl,
responseType: 'arraybuffer',
redirects: 1,
expectedStatus: HttpStatusCode.OK_200
})
await writeFile(options.destination, res.body)
}
async deleteAllArchives (options: OverrideCommandOptions & {
userId: number
}) {

View File

@@ -19,244 +19,7 @@ describe('Test config API validators', function () {
let server: PeerTubeServer
let userAccessToken: string
const updateParams: CustomConfig = {
instance: {
name: 'PeerTube updated',
shortDescription: 'my short description',
description: 'my super description',
terms: 'my super terms',
codeOfConduct: 'my super coc',
creationReason: 'my super reason',
moderationInformation: 'my super moderation information',
administrator: 'Kuja',
maintenanceLifetime: 'forever',
businessModel: 'my super business model',
hardwareInformation: '2vCore 3GB RAM',
languages: [ 'en', 'es' ],
categories: [ 1, 2 ],
isNSFW: true,
defaultNSFWPolicy: 'blur',
defaultClientRoute: '/videos/recently-added',
customizations: {
javascript: 'alert("coucou")',
css: 'body { background-color: red; }'
}
},
theme: {
default: 'default'
},
services: {
twitter: {
username: '@MySuperUsername'
}
},
client: {
videos: {
miniature: {
preferAuthorDisplayName: false
}
},
menu: {
login: {
redirectOnSingleExternalAuth: false
}
}
},
cache: {
previews: {
size: 2
},
captions: {
size: 3
},
torrents: {
size: 4
},
storyboards: {
size: 5
}
},
signup: {
enabled: false,
limit: 5,
requiresApproval: false,
requiresEmailVerification: false,
minimumAge: 16
},
admin: {
email: 'superadmin1@example.com'
},
contactForm: {
enabled: false
},
user: {
history: {
videos: {
enabled: true
}
},
videoQuota: 5242881,
videoQuotaDaily: 318742,
defaultChannelName: 'Main $1 channel'
},
videoChannels: {
maxPerUser: 20
},
transcoding: {
enabled: true,
remoteRunners: {
enabled: true
},
allowAdditionalExtensions: true,
allowAudioFiles: true,
concurrency: 1,
threads: 1,
profile: 'vod_profile',
resolutions: {
'0p': false,
'144p': false,
'240p': false,
'360p': true,
'480p': true,
'720p': false,
'1080p': false,
'1440p': false,
'2160p': false
},
alwaysTranscodeOriginalResolution: false,
webVideos: {
enabled: true
},
hls: {
enabled: false
}
},
live: {
enabled: true,
allowReplay: false,
latencySetting: {
enabled: false
},
maxDuration: 30,
maxInstanceLives: -1,
maxUserLives: 50,
transcoding: {
enabled: true,
remoteRunners: {
enabled: true
},
threads: 4,
profile: 'live_profile',
resolutions: {
'144p': true,
'240p': true,
'360p': true,
'480p': true,
'720p': true,
'1080p': true,
'1440p': true,
'2160p': true
},
alwaysTranscodeOriginalResolution: false
}
},
videoStudio: {
enabled: true,
remoteRunners: {
enabled: true
}
},
videoFile: {
update: {
enabled: true
}
},
import: {
videos: {
concurrency: 1,
http: {
enabled: false
},
torrent: {
enabled: false
}
},
videoChannelSynchronization: {
enabled: false,
maxPerUser: 10
},
users: {
enabled: false
}
},
export: {
users: {
enabled: false,
maxUserVideoQuota: 40,
exportExpiration: 10
}
},
trending: {
videos: {
algorithms: {
enabled: [ 'hot', 'most-viewed', 'most-liked' ],
default: 'most-viewed'
}
}
},
autoBlacklist: {
videos: {
ofUsers: {
enabled: false
}
}
},
followers: {
instance: {
enabled: false,
manualApproval: true
}
},
followings: {
instance: {
autoFollowBack: {
enabled: true
},
autoFollowIndex: {
enabled: true,
indexUrl: 'https://index.example.com'
}
}
},
broadcastMessage: {
enabled: true,
dismissable: true,
message: 'super message',
level: 'warning'
},
search: {
remoteUri: {
users: true,
anonymous: true
},
searchIndex: {
enabled: true,
url: 'https://search.joinpeertube.org',
disableLocalSearch: true,
isDefaultSearch: true
}
},
storyboards: {
enabled: false
}
}
let updateParams: CustomConfig
// ---------------------------------------------------------------
@@ -266,6 +29,7 @@ describe('Test config API validators', function () {
server = await createSingleServer(1)
await setAccessTokensToServers([ server ])
updateParams = await server.config.getCustomConfig()
const user = {
username: 'user1',

View File

@@ -1,8 +1,9 @@
import { HttpStatusCode } from '@peertube/peertube-models'
import { HttpStatusCode, VideoSource } from '@peertube/peertube-models'
import {
PeerTubeServer,
cleanupTests,
createSingleServer,
PeerTubeServer,
makeRawRequest,
setAccessTokensToServers,
setDefaultVideoChannel,
waitJobs
@@ -148,6 +149,66 @@ describe('Test video sources API validator', function () {
})
})
describe('When downloading the source file', function () {
let videoFileToken: string
let videoId: string
let source: VideoSource
let user3: string
let user4: string
before(async function () {
this.timeout(60000)
user3 = await server.users.generateUserAndToken('user3')
user4 = await server.users.generateUserAndToken('user4')
await server.config.enableMinimumTranscoding({ hls: true, keepOriginal: true, webVideo: true })
const { uuid } = await server.videos.quickUpload({ name: 'video', token: user3 })
videoId = uuid
videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid, token: user3 })
await waitJobs([ server ])
source = await server.videos.getSource({ id: videoId, token: user3 })
})
it('Should fail with an invalid filename', async function () {
await makeRawRequest({ url: server.url + '/download/original-video-files/hello.mp4', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
})
it('Should fail without header token or video file token', async function () {
await makeRawRequest({ url: source.fileDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
})
it('Should fail with an invalid header token', async function () {
await makeRawRequest({ url: source.fileDownloadUrl, token: 'toto', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
})
it('Should fail with an invalid video file token', async function () {
await makeRawRequest({ url: source.fileDownloadUrl, query: { videoFileToken: 'toto' }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
})
it('Should fail with header token of another user', async function () {
await makeRawRequest({ url: source.fileDownloadUrl, token: user4, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
})
it('Should fail with video file token of another user', async function () {
const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid, token: user4 })
await makeRawRequest({ url: source.fileDownloadUrl, query: { videoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
})
it('Should succeed with a valid header token', async function () {
await makeRawRequest({ url: source.fileDownloadUrl, token: user3, expectedStatus: HttpStatusCode.OK_200 })
})
it('Should succeed with a valid header token', async function () {
await makeRawRequest({ url: source.fileDownloadUrl, query: { videoFileToken }, expectedStatus: HttpStatusCode.OK_200 })
})
})
after(async function () {
await cleanupTests([ server ])
})

View File

@@ -84,6 +84,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true
expect(data.transcoding.webVideos.enabled).to.be.true
expect(data.transcoding.hls.enabled).to.be.true
expect(data.transcoding.originalFile.keep).to.be.false
expect(data.live.enabled).to.be.false
expect(data.live.allowReplay).to.be.false
@@ -205,6 +206,7 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false
expect(data.transcoding.hls.enabled).to.be.false
expect(data.transcoding.webVideos.enabled).to.be.true
expect(data.transcoding.originalFile.keep).to.be.true
expect(data.live.enabled).to.be.true
expect(data.live.allowReplay).to.be.true
@@ -349,6 +351,9 @@ const newCustomConfig: CustomConfig = {
remoteRunners: {
enabled: true
},
originalFile: {
keep: true
},
allowAdditionalExtensions: true,
allowAudioFiles: true,
threads: 1,

View File

@@ -1,14 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
import {
cleanupTests, getRedirectionUrl, makeActivityPubRawRequest,
makeRawRequest,
ObjectStorageCommand,
PeerTubeServer,
waitJobs
} from '@peertube/peertube-server-commands'
import { expect } from 'chai'
import { wait } from '@peertube/peertube-core-utils'
import {
AccountExportJSON, ActivityPubActor,
ActivityPubOrderedCollection,
@@ -34,6 +26,15 @@ import {
VideoPlaylistType,
VideoPrivacy
} from '@peertube/peertube-models'
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
import {
cleanupTests, getRedirectionUrl, makeActivityPubRawRequest,
makeRawRequest,
ObjectStorageCommand,
PeerTubeServer,
waitJobs
} from '@peertube/peertube-server-commands'
import { expectStartWith } from '@tests/shared/checks.js'
import {
checkExportFileExists,
checkFileExistsInZIP,
@@ -44,8 +45,8 @@ import {
prepareImportExportTests,
regenerateExport
} from '@tests/shared/import-export.js'
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
import { wait } from '@peertube/peertube-core-utils'
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
import { expect } from 'chai'
function runTest (withObjectStorage: boolean) {
let server: PeerTubeServer
@@ -69,10 +70,12 @@ function runTest (withObjectStorage: boolean) {
let noahExportId: number
let objectStorage: ObjectStorageCommand
before(async function () {
this.timeout(240000)
const objectStorage = withObjectStorage
objectStorage = withObjectStorage
? new ObjectStorageCommand()
: undefined;
@@ -126,6 +129,10 @@ function runTest (withObjectStorage: boolean) {
expect(data[0].size).to.be.greaterThan(0)
expect(data[0].state.id).to.equal(UserExportState.COMPLETED)
expect(data[0].state.label).to.equal('Completed')
if (objectStorage) {
expectStartWith(await getRedirectionUrl(data[0].privateDownloadUrl), objectStorage.getMockUserExportBaseUrl())
}
}
await waitJobs([ server ])
@@ -526,6 +533,14 @@ function runTest (withObjectStorage: boolean) {
for (const url of urls) {
await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
}
expect(publicVideo.source.inputFilename).to.equal('video_short.webm')
expect(publicVideo.source.fps).to.equal(25)
expect(publicVideo.source.height).to.equal(720)
expect(publicVideo.source.width).to.equal(1280)
expect(publicVideo.source.metadata?.streams).to.exist
expect(publicVideo.source.resolution).to.equal(720)
expect(publicVideo.source.size).to.equal(218910)
}
{

View File

@@ -1,11 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
import {
cleanupTests, makeRawRequest,
ObjectStorageCommand,
PeerTubeServer, waitJobs
} from '@peertube/peertube-server-commands'
import {
HttpStatusCode,
LiveVideoLatencyMode,
@@ -17,14 +11,20 @@ import {
VideoPrivacy,
VideoState
} from '@peertube/peertube-models'
import { prepareImportExportTests } from '@tests/shared/import-export.js'
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
import { writeFile } from 'fs/promises'
import { join } from 'path'
import { expect } from 'chai'
import { testImage, testAvatarSize } from '@tests/shared/checks.js'
import { completeVideoCheck } from '@tests/shared/videos.js'
import {
ObjectStorageCommand,
PeerTubeServer,
cleanupTests,
waitJobs
} from '@peertube/peertube-server-commands'
import { testAvatarSize, testImage } from '@tests/shared/checks.js'
import { prepareImportExportTests } from '@tests/shared/import-export.js'
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js'
import { completeVideoCheck } from '@tests/shared/videos.js'
import { expect } from 'chai'
import { join } from 'path'
function runTest (withObjectStorage: boolean) {
let server: PeerTubeServer
@@ -115,17 +115,8 @@ function runTest (withObjectStorage: boolean) {
await server.userExports.request({ userId: noahId, withVideoFiles: true })
await server.userExports.waitForCreation({ userId: noahId })
const { data } = await server.userExports.list({ userId: noahId })
const res = await makeRawRequest({
url: data[0].privateDownloadUrl,
responseType: 'arraybuffer',
redirects: 1,
expectedStatus: HttpStatusCode.OK_200
})
archivePath = join(server.getDirectoryPath('tmp'), 'archive.zip')
await writeFile(archivePath, res.body)
await server.userExports.downloadLatestArchive({ userId: noahId, destination: archivePath })
})
it('Should import an archive with video files', async function () {
@@ -444,6 +435,11 @@ function runTest (withObjectStorage: boolean) {
const source = await remoteServer.videos.getSource({ id: otherVideo.uuid })
expect(source.filename).to.equal('video_short.webm')
expect(source.inputFilename).to.equal('video_short.webm')
expect(source.fileDownloadUrl).to.not.exist
expect(source.metadata?.format).to.exist
expect(source.metadata?.streams).to.be.an('array')
}
{
@@ -572,6 +568,57 @@ function runTest (withObjectStorage: boolean) {
}
})
it('Should import original file if included in the export', async function () {
this.timeout(120000)
await server.config.enableMinimumTranscoding({ keepOriginal: true })
await remoteServer.config.keepSourceFile()
const archivePath = join(server.getDirectoryPath('tmp'), 'archive2.zip')
const fixture = 'video_short1.webm'
{
const { token, userId } = await server.users.generate('claire')
await server.videos.quickUpload({ name: 'claire video', token, fixture })
await waitJobs([ server ])
await server.userExports.request({ userId, token, withVideoFiles: true })
await server.userExports.waitForCreation({ userId, token })
await server.userExports.downloadLatestArchive({ userId, token, destination: archivePath })
}
{
const { token, userId } = await remoteServer.users.generate('external_claire')
await remoteServer.userImports.importArchive({ fixture: archivePath, userId, token })
await waitJobs([ remoteServer ])
{
const { data } = await remoteServer.videos.listMyVideos({ token })
expect(data).to.have.lengthOf(1)
const source = await remoteServer.videos.getSource({ id: data[0].id })
expect(source.filename).to.equal(fixture)
expect(source.inputFilename).to.equal(fixture)
expect(source.fileDownloadUrl).to.exist
expect(source.metadata?.format).to.exist
expect(source.metadata?.streams).to.be.an('array')
expect(source.metadata.format['format_name']).to.include('webm')
expect(source.createdAt).to.exist
expect(source.fps).to.equal(25)
expect(source.height).to.equal(720)
expect(source.width).to.equal(1280)
expect(source.resolution.id).to.equal(720)
expect(source.size).to.equal(572456)
}
}
})
after(async function () {
MockSmtpServer.Instance.kill()

View File

@@ -1,24 +1,26 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai'
import { getAllFiles } from '@peertube/peertube-core-utils'
import { HttpStatusCode } from '@peertube/peertube-models'
import { expectStartWith } from '@tests/shared/checks.js'
import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
import {
cleanupTests,
createMultipleServers,
doubleFollow,
makeGetRequest,
makeRawRequest,
ObjectStorageCommand,
PeerTubeServer,
cleanupTests,
createMultipleServers,
doubleFollow, makeGetRequest,
makeRawRequest,
setAccessTokensToServers,
setDefaultAccountAvatar,
setDefaultVideoChannel,
waitJobs
} from '@peertube/peertube-server-commands'
import { expectStartWith } from '@tests/shared/checks.js'
import { checkDirectoryIsEmpty } from '@tests/shared/directories.js'
import { FIXTURE_URLS } from '@tests/shared/tests.js'
import { checkSourceFile } from '@tests/shared/videos.js'
import { expect } from 'chai'
describe('Test a video file replacement', function () {
describe('Test video source management', function () {
let servers: PeerTubeServer[] = []
let replaceDate: Date
@@ -36,6 +38,7 @@ describe('Test a video file replacement', function () {
await setDefaultAccountAvatar(servers)
await servers[0].config.enableFileUpdate()
await servers[0].config.enableMinimumTranscoding()
userToken = await servers[0].users.generateUserAndToken('user1')
@@ -44,30 +47,95 @@ describe('Test a video file replacement', function () {
})
describe('Getting latest video source', () => {
const fixture = 'video_short.webm'
const fixture1 = 'video_short.webm'
const fixture2 = 'video_short1.webm'
const uuids: string[] = []
it('Should get the source filename with legacy upload', async function () {
it('Should get the source filename with legacy upload with disabled keep original file', async function () {
this.timeout(30000)
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture }, mode: 'legacy' })
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture: fixture1 }, mode: 'legacy' })
uuids.push(uuid)
await waitJobs(servers)
const source = await servers[0].videos.getSource({ id: uuid })
expect(source.filename).to.equal(fixture)
expect(source.filename).to.equal(fixture1)
expect(source.inputFilename).to.equal(fixture1)
expect(source.fileDownloadUrl).to.be.null
expect(source.createdAt).to.exist
expect(source.fps).to.equal(25)
expect(source.height).to.equal(720)
expect(source.width).to.equal(1280)
expect(source.resolution.id).to.equal(720)
expect(source.size).to.equal(218910)
expect(source.metadata?.format).to.exist
expect(source.metadata?.streams).to.be.an('array')
await checkDirectoryIsEmpty(servers[0], 'original-video-files')
})
it('Should get the source filename with resumable upload', async function () {
it('Should get the source filename with resumable upload and enabled keep original file', async function () {
this.timeout(30000)
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture }, mode: 'resumable' })
await servers[0].config.keepSourceFile()
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture: fixture2 }, mode: 'resumable' })
uuids.push(uuid)
await waitJobs(servers)
const source = await servers[0].videos.getSource({ id: uuid })
expect(source.filename).to.equal(fixture)
expect(source.filename).to.equal(fixture2)
expect(source.inputFilename).to.equal(fixture2)
expect(source.fileDownloadUrl).to.exist
expect(source.createdAt).to.exist
expect(source.fps).to.equal(25)
expect(source.height).to.equal(720)
expect(source.width).to.equal(1280)
expect(source.resolution.id).to.equal(720)
expect(source.size).to.equal(572456)
expect(source.metadata?.format).to.exist
expect(source.metadata?.streams).to.be.an('array')
})
after(async function () {
it('Should have kept original video file', async function () {
await checkSourceFile({ server: servers[0], fsCount: 1, fixture: fixture2, uuid: uuids[uuids.length - 1] })
})
it('Should transcode a file but do not replace original file', async function () {
await servers[0].videos.runTranscoding({ transcodingType: 'web-video', videoId: uuids[0] })
await servers[0].videos.runTranscoding({ transcodingType: 'web-video', videoId: uuids[1] })
await checkSourceFile({ server: servers[0], fsCount: 1, fixture: fixture2, uuid: uuids[uuids.length - 1] })
})
it('Should also keep audio files', async function () {
const fixture = 'sample.ogg'
const { uuid } = await servers[0].videos.quickUpload({ name: 'audio', fixture })
uuids.push(uuid)
await waitJobs(servers)
const source = await checkSourceFile({ server: servers[0], fsCount: 2, fixture, uuid })
expect(source.createdAt).to.exist
expect(source.fps).to.equal(0)
expect(source.height).to.equal(0)
expect(source.width).to.equal(0)
expect(source.resolution.id).to.equal(0)
expect(source.resolution.label).to.equal('Audio')
expect(source.size).to.equal(105243)
expect(source.metadata?.format).to.exist
expect(source.metadata?.streams).to.be.an('array')
})
it('Should delete all videos and do not have original files anymore', async function () {
this.timeout(60000)
for (const uuid of uuids) {
@@ -75,6 +143,23 @@ describe('Test a video file replacement', function () {
}
await waitJobs(servers)
await checkDirectoryIsEmpty(servers[0], 'original-video-files')
})
it('Should not have source on import', async function () {
const { video: { uuid } } = await servers[0].videoImports.importVideo({
attributes: {
channelId: servers[0].store.channel.id,
targetUrl: FIXTURE_URLS.goodVideo,
privacy: VideoPrivacy.PUBLIC
}
})
await waitJobs(servers)
await servers[0].videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
await checkDirectoryIsEmpty(servers[0], 'original-video-files')
})
})
@@ -110,18 +195,25 @@ describe('Test a video file replacement', function () {
}
})
it('Should not have kept original video file', async function () {
await checkDirectoryIsEmpty(servers[0], 'original-video-files')
})
it('Should replace a video file with transcoding enabled', async function () {
this.timeout(240000)
const previousPaths: string[] = []
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true })
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true, keepOriginal: true })
const { uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'fs with transcoding', fixture: 'video_short_720p.mp4' })
const uploadFixture = 'video_short_720p.mp4'
const { uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'fs with transcoding', fixture: uploadFixture })
uuid = videoUUID
await waitJobs(servers)
await checkSourceFile({ server: servers[0], fsCount: 1, uuid, fixture: uploadFixture })
for (const server of servers) {
const video = await server.videos.get({ id: uuid })
expect(video.inputFileUpdatedAt).to.be.null
@@ -151,9 +243,23 @@ describe('Test a video file replacement', function () {
replaceDate = new Date()
await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' })
const replaceFixture = 'video_short_360p.mp4'
await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: replaceFixture })
await waitJobs(servers)
const source = await checkSourceFile({ server: servers[0], fsCount: 1, uuid, fixture: replaceFixture })
expect(source.createdAt).to.exist
expect(source.fps).to.equal(25)
expect(source.height).to.equal(360)
expect(source.width).to.equal(640)
expect(source.resolution.id).to.equal(360)
expect(source.resolution.label).to.equal('360p')
expect(source.size).to.equal(30620)
expect(source.metadata?.format).to.exist
expect(source.metadata?.streams).to.be.an('array')
for (const server of servers) {
const video = await server.videos.get({ id: uuid })
@@ -189,35 +295,36 @@ describe('Test a video file replacement', function () {
}
}
await servers[0].config.enableMinimumTranscoding()
await servers[0].config.enableMinimumTranscoding({ keepOriginal: true })
})
it('Should have cleaned up old files', async function () {
{
const count = await servers[0].servers.countFiles('storyboards')
expect(count).to.equal(2)
expect(count).to.equal(3)
}
{
const count = await servers[0].servers.countFiles('web-videos')
expect(count).to.equal(5 + 1) // +1 for private directory
expect(count).to.equal(6 + 1) // +1 for private directory
}
{
const count = await servers[0].servers.countFiles('streaming-playlists/hls')
expect(count).to.equal(1 + 1) // +1 for private directory
expect(count).to.equal(2 + 1) // +1 for private directory
}
{
const count = await servers[0].servers.countFiles('torrents')
expect(count).to.equal(9)
expect(count).to.equal(11)
}
})
it('Should have the correct source input', async function () {
it('Should have the correct source input filename', async function () {
const source = await servers[0].videos.getSource({ id: uuid })
expect(source.filename).to.equal('video_short_360p.mp4')
expect(source.inputFilename).to.equal('video_short_360p.mp4')
expect(new Date(source.createdAt)).to.be.above(replaceDate)
})
@@ -367,6 +474,9 @@ describe('Test a video file replacement', function () {
expect(files[0].resolution.id).to.equal(360)
expectStartWith(files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
}
const source = await servers[0].videos.getSource({ id: uuid })
expect(source.fileDownloadUrl).to.not.exist
})
it('Should replace a video file with transcoding enabled', async function () {
@@ -374,16 +484,25 @@ describe('Test a video file replacement', function () {
const previousPaths: string[] = []
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true })
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true, keepOriginal: true })
const fixture1 = 'video_short_360p.mp4'
const { uuid: videoUUID } = await servers[0].videos.quickUpload({
name: 'object storage with transcoding',
fixture: 'video_short_360p.mp4'
fixture: fixture1
})
uuid = videoUUID
await waitJobs(servers)
await checkSourceFile({
server: servers[0],
fixture: fixture1,
fsCount: 0,
uuid,
objectStorageBaseUrl: objectStorage?.getMockOriginalFileBaseUrl()
})
for (const server of servers) {
const video = await server.videos.get({ id: uuid })
@@ -403,9 +522,18 @@ describe('Test a video file replacement', function () {
}
}
await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_240p.mp4' })
const fixture2 = 'video_short_240p.mp4'
await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: fixture2 })
await waitJobs(servers)
await checkSourceFile({
server: servers[0],
fixture: fixture2,
fsCount: 0,
uuid,
objectStorageBaseUrl: objectStorage?.getMockOriginalFileBaseUrl()
})
for (const server of servers) {
const video = await server.videos.get({ id: uuid })

View File

@@ -1,4 +1,5 @@
export * from './client-cli.js'
export * from './live-transcoding.js'
export * from './replace-file.js'
export * from './studio-transcoding.js'
export * from './vod-transcoding.js'

View File

@@ -0,0 +1,86 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { getAllFiles } from '@peertube/peertube-core-utils'
import {
cleanupTests,
createSingleServer,
PeerTubeServer,
setAccessTokensToServers,
setDefaultVideoChannel,
waitJobs
} from '@peertube/peertube-server-commands'
import { PeerTubeRunnerProcess } from '@tests/shared/peertube-runner-process.js'
import { checkSourceFile } from '@tests/shared/videos.js'
import { expect } from 'chai'
describe('Test replace file using peertube-runner program', function () {
let server: PeerTubeServer
let peertubeRunner: PeerTubeRunnerProcess
let uuid: string
before(async function () {
this.timeout(120_000)
server = await createSingleServer(1)
await setAccessTokensToServers([ server ])
await setDefaultVideoChannel([ server ])
await server.config.enableRemoteTranscoding()
await server.config.enableFileUpdate()
await server.config.enableMinimumTranscoding({ hls: true, keepOriginal: true, webVideo: true })
const registrationToken = await server.runnerRegistrationTokens.getFirstRegistrationToken()
peertubeRunner = new PeerTubeRunnerProcess(server)
await peertubeRunner.runServer()
await peertubeRunner.registerPeerTubeInstance({ registrationToken, runnerName: 'runner' })
})
it('Should upload a webm video, transcode it and keep original file', async function () {
this.timeout(240000)
const fixture = 'video_short.webm';
({ uuid } = await server.videos.quickUpload({ name: 'video', fixture }))
await waitJobs(server, { runnerJobs: true })
const video = await server.videos.get({ id: uuid })
const files = getAllFiles(video)
expect(files).to.have.lengthOf(4)
expect(files[0].resolution.id).to.equal(720)
await checkSourceFile({ server, fsCount: 1, fixture, uuid })
})
it('Should upload an audio file, transcode it and keep original file', async function () {
const fixture = 'sample.ogg'
const { uuid } = await server.videos.quickUpload({ name: 'audio', fixture })
await waitJobs([ server ], { runnerJobs: true })
await checkSourceFile({ server, fsCount: 2, fixture, uuid })
})
it('Should replace the video', async function () {
const fixture = 'video_short_360p.mp4'
await server.videos.replaceSourceFile({ videoId: uuid, fixture })
await waitJobs(server, { runnerJobs: true })
const video = await server.videos.get({ id: uuid })
const files = getAllFiles(video)
expect(files).to.have.lengthOf(4)
expect(files[0].resolution.id).to.equal(360)
await checkSourceFile({ server, fsCount: 2, fixture, uuid })
})
after(async function () {
if (peertubeRunner) {
await peertubeRunner.unregisterPeerTubeInstance({ runnerName: 'runner' })
peertubeRunner.kill()
}
await cleanupTests([ server ])
})
})

View File

@@ -1,23 +1,23 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
import { uuidRegex } from '@peertube/peertube-core-utils'
import { HttpStatusCode, HttpStatusCodeType, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@peertube/peertube-models'
import { buildAbsoluteFixturePath, getFileSize, getFilenameFromUrl, getLowercaseExtension } from '@peertube/peertube-node-utils'
import { PeerTubeServer, VideoEdit, getRedirectionUrl, makeRawRequest, waitJobs } from '@peertube/peertube-server-commands'
import {
VIDEO_CATEGORIES,
VIDEO_LANGUAGES,
VIDEO_LICENCES,
VIDEO_PRIVACIES,
loadLanguages
} from '@peertube/peertube-server/core/initializers/constants.js'
import { expect } from 'chai'
import { pathExists } from 'fs-extra/esm'
import { readdir } from 'fs/promises'
import { basename, join } from 'path'
import { uuidRegex } from '@peertube/peertube-core-utils'
import { HttpStatusCode, HttpStatusCodeType, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@peertube/peertube-models'
import {
loadLanguages,
VIDEO_CATEGORIES,
VIDEO_LANGUAGES,
VIDEO_LICENCES,
VIDEO_PRIVACIES
} from '@peertube/peertube-server/core/initializers/constants.js'
import { getLowercaseExtension } from '@peertube/peertube-node-utils'
import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs } from '@peertube/peertube-server-commands'
import { dateIsValid, expectStartWith, testImageGeneratedByFFmpeg } from './checks.js'
import { checkWebTorrentWorks } from './webtorrent.js'
import { completeCheckHlsPlaylist } from './streaming-playlists.js'
import { checkWebTorrentWorks } from './webtorrent.js'
export async function completeWebVideoFilesCheck (options: {
server: PeerTubeServer
@@ -369,3 +369,40 @@ export async function uploadRandomVideoOnServers (
return res
}
export async function checkSourceFile (options: {
server: PeerTubeServer
fsCount: number
uuid: string
fixture: string
objectStorageBaseUrl?: string // default false
}) {
const { server, fsCount, fixture, uuid, objectStorageBaseUrl } = options
const source = await server.videos.getSource({ id: uuid })
const fixtureFileSize = await getFileSize(buildAbsoluteFixturePath(fixture))
if (fsCount > 0) {
expect(await server.servers.countFiles('original-video-files')).to.equal(fsCount)
const keptFilePath = join(server.servers.buildDirectory('original-video-files'), getFilenameFromUrl(source.fileDownloadUrl))
expect(await getFileSize(keptFilePath)).to.equal(fixtureFileSize)
}
expect(source.fileDownloadUrl).to.exist
if (objectStorageBaseUrl) {
const token = await server.videoToken.getVideoFileToken({ videoId: uuid })
expectStartWith(await getRedirectionUrl(source.fileDownloadUrl + '?videoFileToken=' + token), objectStorageBaseUrl)
}
const { body } = await makeRawRequest({
url: source.fileDownloadUrl,
token: server.accessToken,
redirects: 1,
expectedStatus: HttpStatusCode.OK_200
})
expect(body).to.have.lengthOf(fixtureFileSize)
return source
}