From 0305db28c98fd6cf43a3c50ba92c76215e99d512 Mon Sep 17 00:00:00 2001 From: Jelle Besseling Date: Tue, 17 Aug 2021 08:26:20 +0200 Subject: Add support for saving video files to object storage (#4290) * Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz --- shared/extra-utils/miscs/checks.ts | 7 +- shared/extra-utils/miscs/tests.ts | 15 ++++- shared/extra-utils/mock-servers/index.ts | 1 + .../mock-servers/mock-object-storage.ts | 42 ++++++++++++ shared/extra-utils/requests/requests.ts | 16 +++++ shared/extra-utils/server/config-command.ts | 72 ++++++++++++++++++++ shared/extra-utils/server/index.ts | 1 + shared/extra-utils/server/jobs-command.ts | 10 +++ shared/extra-utils/server/jobs.ts | 6 +- .../extra-utils/server/object-storage-command.ts | 77 ++++++++++++++++++++++ shared/extra-utils/server/server.ts | 18 ++++- shared/extra-utils/server/servers.ts | 4 +- shared/extra-utils/videos/live-command.ts | 2 +- shared/extra-utils/videos/live.ts | 7 ++ .../videos/streaming-playlists-command.ts | 6 +- shared/extra-utils/videos/streaming-playlists.ts | 7 +- shared/extra-utils/videos/videos-command.ts | 11 ++++ shared/models/server/job.model.ts | 6 ++ shared/models/videos/index.ts | 1 + shared/models/videos/video-state.enum.ts | 3 +- shared/models/videos/video-storage.enum.ts | 4 ++ 21 files changed, 299 insertions(+), 17 deletions(-) create mode 100644 shared/extra-utils/mock-servers/mock-object-storage.ts create mode 100644 shared/extra-utils/server/object-storage-command.ts create mode 100644 shared/models/videos/video-storage.enum.ts (limited to 'shared') diff --git a/shared/extra-utils/miscs/checks.ts b/shared/extra-utils/miscs/checks.ts index 7fc92f804..aa2c8e8fa 100644 --- a/shared/extra-utils/miscs/checks.ts +++ b/shared/extra-utils/miscs/checks.ts @@ -16,6 +16,10 @@ function dateIsValid (dateString: string, interval = 300000) { return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval } +function expectStartWith (str: string, start: string) { + expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.true +} + async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') { const res = await makeGetRequest({ url, @@ -42,5 +46,6 @@ async function testFileExistsOrNot (server: PeerTubeServer, directory: string, f export { dateIsValid, testImage, - testFileExistsOrNot + testFileExistsOrNot, + expectStartWith } diff --git a/shared/extra-utils/miscs/tests.ts b/shared/extra-utils/miscs/tests.ts index 3dfb2487e..dd86041fe 100644 --- a/shared/extra-utils/miscs/tests.ts +++ b/shared/extra-utils/miscs/tests.ts @@ -28,7 +28,9 @@ const FIXTURE_URLS = { badVideo: 'https://download.cpy.re/peertube/bad_video.mp4', goodVideo: 'https://download.cpy.re/peertube/good_video.mp4', - video4K: 'https://download.cpy.re/peertube/4k_file.txt' + goodVideo720: 'https://download.cpy.re/peertube/good_video_720.mp4', + + file4K: 'https://download.cpy.re/peertube/4k_file.txt' } function parallelTests () { @@ -42,7 +44,15 @@ function isGithubCI () { function areHttpImportTestsDisabled () { const disabled = process.env.DISABLE_HTTP_IMPORT_TESTS === 'true' - if (disabled) console.log('Import tests are disabled') + if (disabled) console.log('DISABLE_HTTP_IMPORT_TESTS env set to "true" so import tests are disabled') + + return disabled +} + +function areObjectStorageTestsDisabled () { + const disabled = process.env.ENABLE_OBJECT_STORAGE_TESTS !== 'true' + + if (disabled) console.log('ENABLE_OBJECT_STORAGE_TESTS env is not set to "true" so object storage tests are disabled') return disabled } @@ -89,6 +99,7 @@ export { buildAbsoluteFixturePath, getFileSize, buildRequestStub, + areObjectStorageTestsDisabled, wait, root } diff --git a/shared/extra-utils/mock-servers/index.ts b/shared/extra-utils/mock-servers/index.ts index 0ec07f685..93c00c788 100644 --- a/shared/extra-utils/mock-servers/index.ts +++ b/shared/extra-utils/mock-servers/index.ts @@ -2,3 +2,4 @@ export * from './mock-email' export * from './mock-instances-index' export * from './mock-joinpeertube-versions' export * from './mock-plugin-blocklist' +export * from './mock-object-storage' diff --git a/shared/extra-utils/mock-servers/mock-object-storage.ts b/shared/extra-utils/mock-servers/mock-object-storage.ts new file mode 100644 index 000000000..19ea7c87c --- /dev/null +++ b/shared/extra-utils/mock-servers/mock-object-storage.ts @@ -0,0 +1,42 @@ +import * as express from 'express' +import got, { RequestError } from 'got' +import { Server } from 'http' +import { pipeline } from 'stream' +import { randomInt } from '@shared/core-utils' +import { ObjectStorageCommand } from '../server' + +export class MockObjectStorage { + private server: Server + + initialize () { + return new Promise(res => { + const app = express() + + app.get('/:bucketName/:path(*)', (req: express.Request, res: express.Response, next: express.NextFunction) => { + const url = `http://${req.params.bucketName}.${ObjectStorageCommand.getEndpointHost()}/${req.params.path}` + + if (process.env.DEBUG) { + console.log('Receiving request on mocked server %s.', req.url) + console.log('Proxifying request to %s', url) + } + + return pipeline( + got.stream(url, { throwHttpErrors: false }), + res, + (err: RequestError) => { + if (!err) return + + console.error('Pipeline failed.', err) + } + ) + }) + + const port = 42301 + randomInt(1, 100) + this.server = app.listen(port, () => res(port)) + }) + } + + terminate () { + if (this.server) this.server.close() + } +} diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts index 70f790222..e3ecd1af2 100644 --- a/shared/extra-utils/requests/requests.ts +++ b/shared/extra-utils/requests/requests.ts @@ -121,6 +121,20 @@ function unwrapText (test: request.Test): Promise { return test.then(res => res.text) } +function unwrapBodyOrDecodeToJSON (test: request.Test): Promise { + return test.then(res => { + if (res.body instanceof Buffer) { + return JSON.parse(new TextDecoder().decode(res.body)) + } + + return res.body + }) +} + +function unwrapTextOrDecode (test: request.Test): Promise { + return test.then(res => res.text || new TextDecoder().decode(res.body)) +} + // --------------------------------------------------------------------------- export { @@ -134,6 +148,8 @@ export { makeRawRequest, makeActivityPubGetRequest, unwrapBody, + unwrapTextOrDecode, + unwrapBodyOrDecodeToJSON, unwrapText } diff --git a/shared/extra-utils/server/config-command.ts b/shared/extra-utils/server/config-command.ts index 11148aa46..51d04fa63 100644 --- a/shared/extra-utils/server/config-command.ts +++ b/shared/extra-utils/server/config-command.ts @@ -18,6 +18,70 @@ export class ConfigCommand extends AbstractCommand { } } + enableImports () { + return this.updateExistingSubConfig({ + newConfig: { + import: { + videos: { + http: { + enabled: true + }, + + torrent: { + enabled: true + } + } + } + } + }) + } + + enableLive (options: { + allowReplay?: boolean + transcoding?: boolean + } = {}) { + return this.updateExistingSubConfig({ + newConfig: { + live: { + enabled: true, + allowReplay: options.allowReplay ?? true, + transcoding: { + enabled: options.transcoding ?? true, + resolutions: ConfigCommand.getCustomConfigResolutions(true) + } + } + } + }) + } + + disableTranscoding () { + return this.updateExistingSubConfig({ + newConfig: { + transcoding: { + enabled: false + } + } + }) + } + + enableTranscoding (webtorrent = true, hls = true) { + return this.updateExistingSubConfig({ + newConfig: { + transcoding: { + enabled: true, + resolutions: ConfigCommand.getCustomConfigResolutions(true), + + webtorrent: { + enabled: webtorrent + }, + hls: { + enabled: hls + } + } + } + }) + } + getConfig (options: OverrideCommandOptions = {}) { const path = '/api/v1/config' @@ -81,6 +145,14 @@ export class ConfigCommand extends AbstractCommand { }) } + async updateExistingSubConfig (options: OverrideCommandOptions & { + newConfig: DeepPartial + }) { + const existing = await this.getCustomConfig(options) + + return this.updateCustomConfig({ ...options, newCustomConfig: merge({}, existing, options.newConfig) }) + } + updateCustomSubConfig (options: OverrideCommandOptions & { newConfig: DeepPartial }) { diff --git a/shared/extra-utils/server/index.ts b/shared/extra-utils/server/index.ts index 9055dfc57..92ff7a0f9 100644 --- a/shared/extra-utils/server/index.ts +++ b/shared/extra-utils/server/index.ts @@ -6,6 +6,7 @@ export * from './follows-command' export * from './follows' export * from './jobs' export * from './jobs-command' +export * from './object-storage-command' export * from './plugins-command' export * from './plugins' export * from './redundancy-command' diff --git a/shared/extra-utils/server/jobs-command.ts b/shared/extra-utils/server/jobs-command.ts index c4eb12dc2..91771c176 100644 --- a/shared/extra-utils/server/jobs-command.ts +++ b/shared/extra-utils/server/jobs-command.ts @@ -5,6 +5,16 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared' export class JobsCommand extends AbstractCommand { + async getLatest (options: OverrideCommandOptions & { + jobType: JobType + }) { + const { data } = await this.getJobsList({ ...options, start: 0, count: 1, sort: '-createdAt' }) + + if (data.length === 0) return undefined + + return data[0] + } + getJobsList (options: OverrideCommandOptions & { state?: JobState jobType?: JobType diff --git a/shared/extra-utils/server/jobs.ts b/shared/extra-utils/server/jobs.ts index 64a0353eb..27104bfdf 100644 --- a/shared/extra-utils/server/jobs.ts +++ b/shared/extra-utils/server/jobs.ts @@ -3,7 +3,7 @@ import { JobState } from '../../models' import { wait } from '../miscs' import { PeerTubeServer } from './server' -async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer) { +async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer, skipDelayed = false) { const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) : 250 @@ -13,7 +13,9 @@ async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer) { if (Array.isArray(serversArg) === false) servers = [ serversArg as PeerTubeServer ] else servers = serversArg as PeerTubeServer[] - const states: JobState[] = [ 'waiting', 'active', 'delayed' ] + const states: JobState[] = [ 'waiting', 'active' ] + if (!skipDelayed) states.push('delayed') + const repeatableJobs = [ 'videos-views', 'activitypub-cleaner' ] let pendingRequests: boolean diff --git a/shared/extra-utils/server/object-storage-command.ts b/shared/extra-utils/server/object-storage-command.ts new file mode 100644 index 000000000..b4de8f4cb --- /dev/null +++ b/shared/extra-utils/server/object-storage-command.ts @@ -0,0 +1,77 @@ + +import { HttpStatusCode } from '@shared/models' +import { makePostBodyRequest } from '../requests' +import { AbstractCommand } from '../shared' + +export class ObjectStorageCommand extends AbstractCommand { + static readonly DEFAULT_PLAYLIST_BUCKET = 'streaming-playlists' + static readonly DEFAULT_WEBTORRENT_BUCKET = 'videos' + + static getDefaultConfig () { + return { + object_storage: { + enabled: true, + endpoint: 'http://' + this.getEndpointHost(), + region: this.getRegion(), + + credentials: this.getCredentialsConfig(), + + streaming_playlists: { + bucket_name: this.DEFAULT_PLAYLIST_BUCKET + }, + + videos: { + bucket_name: this.DEFAULT_WEBTORRENT_BUCKET + } + } + } + } + + static getCredentialsConfig () { + return { + access_key_id: 'AKIAIOSFODNN7EXAMPLE', + secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' + } + } + + static getEndpointHost () { + return 'localhost:9444' + } + + static getRegion () { + return 'us-east-1' + } + + static getWebTorrentBaseUrl () { + return `http://${this.DEFAULT_WEBTORRENT_BUCKET}.${this.getEndpointHost()}/` + } + + static getPlaylistBaseUrl () { + return `http://${this.DEFAULT_PLAYLIST_BUCKET}.${this.getEndpointHost()}/` + } + + static async prepareDefaultBuckets () { + await this.createBucket(this.DEFAULT_PLAYLIST_BUCKET) + await this.createBucket(this.DEFAULT_WEBTORRENT_BUCKET) + } + + static async createBucket (name: string) { + await makePostBodyRequest({ + url: this.getEndpointHost(), + path: '/ui/' + name + '?delete', + expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 + }) + + await makePostBodyRequest({ + url: this.getEndpointHost(), + path: '/ui/' + name + '?create', + expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 + }) + + await makePostBodyRequest({ + url: this.getEndpointHost(), + path: '/ui/' + name + '?make-public', + expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 + }) + } +} diff --git a/shared/extra-utils/server/server.ts b/shared/extra-utils/server/server.ts index 3c335b8e4..bc5e1cd5f 100644 --- a/shared/extra-utils/server/server.ts +++ b/shared/extra-utils/server/server.ts @@ -38,11 +38,13 @@ import { PluginsCommand } from './plugins-command' import { RedundancyCommand } from './redundancy-command' import { ServersCommand } from './servers-command' import { StatsCommand } from './stats-command' +import { ObjectStorageCommand } from './object-storage-command' export type RunServerOptions = { hideLogs?: boolean nodeArgs?: string[] peertubeArgs?: string[] + env?: { [ id: string ]: string } } export class PeerTubeServer { @@ -121,6 +123,7 @@ export class PeerTubeServer { servers?: ServersCommand login?: LoginCommand users?: UsersCommand + objectStorage?: ObjectStorageCommand videos?: VideosCommand constructor (options: { serverNumber: number } | { url: string }) { @@ -202,6 +205,10 @@ export class PeerTubeServer { env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString() env['NODE_CONFIG'] = JSON.stringify(configOverride) + if (options.env) { + Object.assign(env, options.env) + } + const forkOptions = { silent: true, env, @@ -209,10 +216,17 @@ export class PeerTubeServer { execArgv: options.nodeArgs || [] } - return new Promise(res => { + return new Promise((res, rej) => { const self = this this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions) + + const onExit = function () { + return rej(new Error('Process exited')) + } + + this.app.on('exit', onExit) + this.app.stdout.on('data', function onStdout (data) { let dontContinue = false @@ -241,6 +255,7 @@ export class PeerTubeServer { console.log(data.toString()) } else { self.app.stdout.removeListener('data', onStdout) + self.app.removeListener('exit', onExit) } process.on('exit', () => { @@ -365,5 +380,6 @@ export class PeerTubeServer { this.login = new LoginCommand(this) this.users = new UsersCommand(this) this.videos = new VideosCommand(this) + this.objectStorage = new ObjectStorageCommand(this) } } diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts index f0622feb0..21ab9405b 100644 --- a/shared/extra-utils/server/servers.ts +++ b/shared/extra-utils/server/servers.ts @@ -10,11 +10,11 @@ async function createSingleServer (serverNumber: number, configOverride?: Object return server } -function createMultipleServers (totalServers: number, configOverride?: Object) { +function createMultipleServers (totalServers: number, configOverride?: Object, options: RunServerOptions = {}) { const serverPromises: Promise[] = [] for (let i = 1; i <= totalServers; i++) { - serverPromises.push(createSingleServer(i, configOverride)) + serverPromises.push(createSingleServer(i, configOverride, options)) } return Promise.all(serverPromises) diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts index 81ae458e0..74f5d3089 100644 --- a/shared/extra-utils/videos/live-command.ts +++ b/shared/extra-utils/videos/live-command.ts @@ -126,7 +126,7 @@ export class LiveCommand extends AbstractCommand { video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId }) await wait(500) - } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED) + } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED) } async countPlaylists (options: OverrideCommandOptions & { diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts index 9a6df07a8..29f99ed6d 100644 --- a/shared/extra-utils/videos/live.ts +++ b/shared/extra-utils/videos/live.ts @@ -89,6 +89,12 @@ async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], vi } } +async function waitUntilLiveSavedOnAllServers (servers: PeerTubeServer[], videoId: string) { + for (const server of servers) { + await server.live.waitUntilSaved({ videoId }) + } +} + async function checkLiveCleanupAfterSave (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) { const basePath = server.servers.buildDirectory('streaming-playlists') const hlsPath = join(basePath, 'hls', videoUUID) @@ -126,5 +132,6 @@ export { testFfmpegStreamError, stopFfmpeg, waitUntilLivePublishedOnAllServers, + waitUntilLiveSavedOnAllServers, checkLiveCleanupAfterSave } diff --git a/shared/extra-utils/videos/streaming-playlists-command.ts b/shared/extra-utils/videos/streaming-playlists-command.ts index 9662685da..5d40d35cb 100644 --- a/shared/extra-utils/videos/streaming-playlists-command.ts +++ b/shared/extra-utils/videos/streaming-playlists-command.ts @@ -1,5 +1,5 @@ import { HttpStatusCode } from '@shared/models' -import { unwrapBody, unwrapText } from '../requests' +import { unwrapBody, unwrapTextOrDecode, unwrapBodyOrDecodeToJSON } from '../requests' import { AbstractCommand, OverrideCommandOptions } from '../shared' export class StreamingPlaylistsCommand extends AbstractCommand { @@ -7,7 +7,7 @@ export class StreamingPlaylistsCommand extends AbstractCommand { get (options: OverrideCommandOptions & { url: string }) { - return unwrapText(this.getRawRequest({ + return unwrapTextOrDecode(this.getRawRequest({ ...options, url: options.url, @@ -33,7 +33,7 @@ export class StreamingPlaylistsCommand extends AbstractCommand { getSegmentSha256 (options: OverrideCommandOptions & { url: string }) { - return unwrapBody<{ [ id: string ]: string }>(this.getRawRequest({ + return unwrapBodyOrDecodeToJSON<{ [ id: string ]: string }>(this.getRawRequest({ ...options, url: options.url, diff --git a/shared/extra-utils/videos/streaming-playlists.ts b/shared/extra-utils/videos/streaming-playlists.ts index a224b8f5f..6671e3fa6 100644 --- a/shared/extra-utils/videos/streaming-playlists.ts +++ b/shared/extra-utils/videos/streaming-playlists.ts @@ -9,17 +9,16 @@ async function checkSegmentHash (options: { server: PeerTubeServer baseUrlPlaylist: string baseUrlSegment: string - videoUUID: string resolution: number hlsPlaylist: VideoStreamingPlaylist }) { - const { server, baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist } = options + const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist } = options const command = server.streamingPlaylists const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) const videoName = basename(file.fileUrl) - const playlist = await command.get({ url: `${baseUrlPlaylist}/${videoUUID}/${removeFragmentedMP4Ext(videoName)}.m3u8` }) + const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8` }) const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist) @@ -28,7 +27,7 @@ async function checkSegmentHash (options: { const range = `${offset}-${offset + length - 1}` const segmentBody = await command.getSegment({ - url: `${baseUrlSegment}/${videoUUID}/${videoName}`, + url: `${baseUrlSegment}/${videoName}`, expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206, range: `bytes=${range}` }) diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts index 33725bfdc..d35339c8d 100644 --- a/shared/extra-utils/videos/videos-command.ts +++ b/shared/extra-utils/videos/videos-command.ts @@ -188,6 +188,17 @@ export class VideosCommand extends AbstractCommand { return id } + async listFiles (options: OverrideCommandOptions & { + id: number | string + }) { + const video = await this.get(options) + + const files = video.files || [] + const hlsFiles = video.streamingPlaylists[0]?.files || [] + + return files.concat(hlsFiles) + } + // --------------------------------------------------------------------------- listMyVideos (options: OverrideCommandOptions & { diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts index 4ab249e0b..ff96283a4 100644 --- a/shared/models/server/job.model.ts +++ b/shared/models/server/job.model.ts @@ -19,6 +19,7 @@ export type JobType = | 'video-redundancy' | 'video-live-ending' | 'actor-keys' + | 'move-to-object-storage' export interface Job { id: number @@ -136,3 +137,8 @@ export interface VideoLiveEndingPayload { export interface ActorKeysPayload { actorId: number } + +export interface MoveObjectStoragePayload { + videoUUID: string + isNewVideo: boolean +} diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index faa9b9868..733c433a0 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts @@ -26,6 +26,7 @@ export * from './video-resolution.enum' export * from './video-schedule-update.model' export * from './video-sort-field.type' export * from './video-state.enum' +export * from './video-storage.enum' export * from './video-streaming-playlist.model' export * from './video-streaming-playlist.type' diff --git a/shared/models/videos/video-state.enum.ts b/shared/models/videos/video-state.enum.ts index 49d997f24..c6af481e7 100644 --- a/shared/models/videos/video-state.enum.ts +++ b/shared/models/videos/video-state.enum.ts @@ -3,5 +3,6 @@ export const enum VideoState { TO_TRANSCODE = 2, TO_IMPORT = 3, WAITING_FOR_LIVE = 4, - LIVE_ENDED = 5 + LIVE_ENDED = 5, + TO_MOVE_TO_EXTERNAL_STORAGE = 6 } diff --git a/shared/models/videos/video-storage.enum.ts b/shared/models/videos/video-storage.enum.ts new file mode 100644 index 000000000..7c6690db2 --- /dev/null +++ b/shared/models/videos/video-storage.enum.ts @@ -0,0 +1,4 @@ +export const enum VideoStorage { + FILE_SYSTEM, + OBJECT_STORAGE, +} -- cgit v1.2.3