mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-06 03:50:26 +02:00
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:
parent
ae31e90c30
commit
e57c3024f4
75 changed files with 1653 additions and 801 deletions
|
@ -41,6 +41,7 @@ const customConfigUpdateValidator = [
|
|||
body('videoChannels.maxPerUser').isInt(),
|
||||
|
||||
body('transcoding.enabled').isBoolean(),
|
||||
body('transcoding.originalFile.keep').isBoolean(),
|
||||
body('transcoding.allowAdditionalExtensions').isBoolean(),
|
||||
body('transcoding.threads').isInt(),
|
||||
body('transcoding.concurrency').isInt({ min: 1 }),
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Request, Response } from 'express'
|
||||
import { HttpStatusCode, ServerErrorCode, UserRight, UserRightType, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { exists } from '@server/helpers/custom-validators/misc.js'
|
||||
import { loadVideo, VideoLoadType } from '@server/lib/model-loaders/index.js'
|
||||
import { VideoLoadType, loadVideo } from '@server/lib/model-loaders/index.js'
|
||||
import { isUserQuotaValid } from '@server/lib/user.js'
|
||||
import { VideoTokensManager } from '@server/lib/video-tokens-manager.js'
|
||||
import { authenticatePromise } from '@server/middlewares/auth.js'
|
||||
|
@ -20,10 +19,12 @@ import {
|
|||
MVideoId,
|
||||
MVideoImmutable,
|
||||
MVideoThumbnail,
|
||||
MVideoUUID,
|
||||
MVideoWithRights
|
||||
} from '@server/types/models/index.js'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
|
||||
export async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
|
||||
const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
||||
|
||||
const video = await loadVideo(id, fetchType, userId)
|
||||
|
@ -64,7 +65,7 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
|
||||
export async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
|
||||
if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
|
@ -78,7 +79,7 @@ async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | st
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
|
||||
export async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
|
||||
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
|
||||
|
||||
if (videoChannel === null) {
|
||||
|
@ -105,7 +106,7 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkCanSeeVideo (options: {
|
||||
export async function checkCanSeeVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
paramId: string
|
||||
|
@ -128,7 +129,7 @@ async function checkCanSeeVideo (options: {
|
|||
throw new Error('Unknown video privacy when checking video right ' + video.url)
|
||||
}
|
||||
|
||||
async function checkCanSeeUserAuthVideo (options: {
|
||||
export async function checkCanSeeUserAuthVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
video: MVideoId | MVideoWithRights
|
||||
|
@ -174,7 +175,7 @@ async function checkCanSeeUserAuthVideo (options: {
|
|||
return fail()
|
||||
}
|
||||
|
||||
async function checkCanSeePasswordProtectedVideo (options: {
|
||||
export async function checkCanSeePasswordProtectedVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
video: MVideo
|
||||
|
@ -215,13 +216,13 @@ async function checkCanSeePasswordProtectedVideo (options: {
|
|||
return false
|
||||
}
|
||||
|
||||
function canUserAccessVideo (user: MUser, video: MVideoWithRights | MVideoAccountLight, right: UserRightType) {
|
||||
export function canUserAccessVideo (user: MUser, video: MVideoWithRights | MVideoAccountLight, right: UserRightType) {
|
||||
const isOwnedByUser = video.VideoChannel.Account.userId === user.id
|
||||
|
||||
return isOwnedByUser || user.hasRight(right)
|
||||
}
|
||||
|
||||
async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> {
|
||||
export async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> {
|
||||
return video.VideoChannel?.Account?.userId
|
||||
? video
|
||||
: VideoModel.loadFull(video.id)
|
||||
|
@ -229,7 +230,7 @@ async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithR
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkCanAccessVideoStaticFiles (options: {
|
||||
export async function checkCanAccessVideoStaticFiles (options: {
|
||||
video: MVideo
|
||||
req: Request
|
||||
res: Response
|
||||
|
@ -241,23 +242,51 @@ async function checkCanAccessVideoStaticFiles (options: {
|
|||
return checkCanSeeVideo(options)
|
||||
}
|
||||
|
||||
const videoFileToken = req.query.videoFileToken
|
||||
if (videoFileToken && VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) {
|
||||
const user = VideoTokensManager.Instance.getUserFromToken({ token: videoFileToken })
|
||||
|
||||
res.locals.videoFileToken = { user }
|
||||
return true
|
||||
}
|
||||
assignVideoTokenIfNeeded(req, res, video)
|
||||
|
||||
if (res.locals.videoFileToken) return true
|
||||
if (!video.hasPrivateStaticPath()) return true
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return false
|
||||
}
|
||||
|
||||
export async function checkCanAccessVideoSourceFile (options: {
|
||||
videoId: number
|
||||
req: Request
|
||||
res: Response
|
||||
}) {
|
||||
const { req, res, videoId } = options
|
||||
|
||||
const video = await VideoModel.loadFull(videoId)
|
||||
|
||||
if (res.locals.oauth?.token.User) {
|
||||
if (canUserAccessVideo(res.locals.oauth.token.User, video, UserRight.SEE_ALL_VIDEOS) === true) return true
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return false
|
||||
}
|
||||
|
||||
assignVideoTokenIfNeeded(req, res, video)
|
||||
if (res.locals.videoFileToken) return true
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return false
|
||||
}
|
||||
|
||||
function assignVideoTokenIfNeeded (req: Request, res: Response, video: MVideoUUID) {
|
||||
const videoFileToken = req.query.videoFileToken
|
||||
|
||||
if (videoFileToken && VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) {
|
||||
const user = VideoTokensManager.Instance.getUserFromToken({ token: videoFileToken })
|
||||
|
||||
res.locals.videoFileToken = { user }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRightType, res: Response, onlyOwned = true) {
|
||||
export function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRightType, res: Response, onlyOwned = true) {
|
||||
// Retrieve the user who did the request
|
||||
if (onlyOwned && video.isOwned() === false) {
|
||||
res.fail({
|
||||
|
@ -284,7 +313,7 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right:
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) {
|
||||
export async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) {
|
||||
if (await isUserQuotaValid({ userId: user.id, uploadSize: videoFileSize }) === false) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
|
||||
|
@ -296,16 +325,3 @@ async function checkUserQuota (user: MUserId, videoFileSize: number, res: Respon
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesVideoChannelOfAccountExist,
|
||||
doesVideoExist,
|
||||
doesVideoFileOfVideoExist,
|
||||
|
||||
checkCanAccessVideoStaticFiles,
|
||||
checkUserCanManageVideo,
|
||||
checkCanSeeVideo,
|
||||
checkUserQuota
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import express from 'express'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
||||
import { ffprobePromise, getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
export async function addDurationToVideoFileIfNeeded (options: {
|
||||
|
@ -11,7 +11,7 @@ export async function addDurationToVideoFileIfNeeded (options: {
|
|||
const { res, middlewareName, videoFile } = options
|
||||
|
||||
try {
|
||||
if (!videoFile.duration) await addDurationToVideo(videoFile)
|
||||
if (!videoFile.duration) await addDurationToVideo(res, videoFile)
|
||||
} catch (err) {
|
||||
logger.error('Invalid input file in ' + middlewareName, { err })
|
||||
|
||||
|
@ -29,8 +29,11 @@ export async function addDurationToVideoFileIfNeeded (options: {
|
|||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
|
||||
const duration = await getVideoStreamDuration(videoFile.path)
|
||||
async function addDurationToVideo (res: express.Response, videoFile: { path: string, duration?: number }) {
|
||||
const probe = await ffprobePromise(videoFile.path)
|
||||
res.locals.ffprobe = probe
|
||||
|
||||
const duration = await getVideoStreamDuration(videoFile.path, probe)
|
||||
|
||||
// FFmpeg may not be able to guess video duration
|
||||
// For example with m2v files: https://trac.ffmpeg.org/ticket/9726#comment:2
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
import express from 'express'
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { getVideoWithAttributes } from '@server/helpers/video.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
|
||||
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
||||
import { MVideoFullLight } from '@server/types/models/index.js'
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { Metadata as UploadXMetadata } from '@uploadx/core'
|
||||
import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanAccessVideoSourceFile,
|
||||
checkUserCanManageVideo,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam
|
||||
} from '../shared/index.js'
|
||||
import { addDurationToVideoFileIfNeeded, checkVideoFileCanBeEdited, commonVideoFileChecks, isVideoFileAccepted } from './shared/index.js'
|
||||
|
||||
export const videoSourceGetLatestValidator = [
|
||||
|
@ -71,6 +78,28 @@ export const replaceVideoSourceResumableInitValidator = [
|
|||
}
|
||||
]
|
||||
|
||||
export const originalVideoFileDownloadValidator = [
|
||||
param('filename').exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const videoSource = await VideoSourceModel.loadByKeptOriginalFilename(req.params.filename)
|
||||
if (!videoSource) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Original video file not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (!await checkCanAccessVideoSourceFile({ req, res, videoId: videoSource.videoId })) return
|
||||
|
||||
res.locals.videoSource = videoSource
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import express from 'express'
|
||||
import { body, param, query, ValidationChain } from 'express-validator'
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@peertube/peertube-models'
|
||||
import { Redis } from '@server/lib/redis.js'
|
||||
|
@ -7,6 +5,8 @@ import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
|
|||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
|
||||
import { MUserAccountId, MVideoFullLight } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { ValidationChain, body, param, query } from 'express-validator'
|
||||
import {
|
||||
exists,
|
||||
isBooleanValid,
|
||||
|
@ -41,8 +41,7 @@ import { CONFIG } from '../../../initializers/config.js'
|
|||
import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanAccessVideoStaticFiles,
|
||||
areValidationErrors, checkCanAccessVideoStaticFiles,
|
||||
checkCanSeeVideo,
|
||||
checkUserCanManageVideo,
|
||||
doesVideoChannelOfAccountExist,
|
||||
|
@ -501,23 +500,19 @@ const commonVideosFiltersValidator = [
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videosAddLegacyValidator,
|
||||
videosAddResumableValidator,
|
||||
videosAddResumableInitValidator,
|
||||
|
||||
videosUpdateValidator,
|
||||
videosGetValidator,
|
||||
videoFileMetadataGetValidator,
|
||||
videosDownloadValidator,
|
||||
checkVideoFollowConstraints,
|
||||
videosCustomGetValidator,
|
||||
videosRemoveValidator,
|
||||
|
||||
getCommonVideoEditAttributes,
|
||||
|
||||
commonVideosFiltersValidator,
|
||||
|
||||
videosOverviewValidator
|
||||
getCommonVideoEditAttributes,
|
||||
videoFileMetadataGetValidator,
|
||||
videosAddLegacyValidator,
|
||||
videosAddResumableInitValidator,
|
||||
videosAddResumableValidator,
|
||||
videosCustomGetValidator,
|
||||
videosDownloadValidator,
|
||||
videosGetValidator,
|
||||
videosOverviewValidator,
|
||||
videosRemoveValidator,
|
||||
videosUpdateValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue