From 2e401e8575decb1d491d0db48ca1ab1eba5b2a66 Mon Sep 17 00:00:00 2001 From: kontrollanten <6680299+kontrollanten@users.noreply.github.com> Date: Tue, 21 Jun 2022 15:31:25 +0200 Subject: store uploaded video filename (#4885) * store uploaded video filename closes #4731 * dont crash if videos channel exist * migration: use raw query * video source: fixes after code review * cleanup * bump migration * updates after code review * refactor: use checkUserCanManageVideo * videoSource: add openapi doc * test(check-params/video-source): fix timeout * Styling * Correctly set original filename as source Co-authored-by: Chocobozzz --- server/controllers/api/videos/index.ts | 13 +++++ server/controllers/api/videos/upload.ts | 7 +++ server/initializers/constants.ts | 2 +- server/initializers/database.ts | 2 + .../initializers/migrations/0715-video-source.ts | 34 +++++++++++++ server/middlewares/validators/videos/index.ts | 1 + .../middlewares/validators/videos/video-source.ts | 37 +++++++++++++++ server/middlewares/validators/videos/videos.ts | 2 +- server/models/video/video-source.ts | 55 ++++++++++++++++++++++ server/models/video/video.ts | 10 ++++ server/tests/api/check-params/index.ts | 11 +++-- server/tests/api/check-params/video-source.ts | 44 +++++++++++++++++ server/tests/api/videos/index.ts | 1 + server/tests/api/videos/video-source.ts | 39 +++++++++++++++ server/types/express.d.ts | 6 ++- server/types/models/video/video-source.ts | 3 ++ 16 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 server/initializers/migrations/0715-video-source.ts create mode 100644 server/middlewares/validators/videos/video-source.ts create mode 100644 server/models/video/video-source.ts create mode 100644 server/tests/api/check-params/video-source.ts create mode 100644 server/tests/api/videos/video-source.ts create mode 100644 server/types/models/video/video-source.ts (limited to 'server') diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index be233722c..d4e08293e 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -26,6 +26,7 @@ import { setDefaultVideosSort, videosCustomGetValidator, videosGetValidator, + videoSourceGetValidator, videosRemoveValidator, videosSortValidator } from '../../../middlewares' @@ -96,6 +97,14 @@ videosRouter.get('/:id/description', asyncMiddleware(videosGetValidator), asyncMiddleware(getVideoDescription) ) + +videosRouter.get('/:id/source', + openapiOperationDoc({ operationId: 'getVideoSource' }), + authenticate, + asyncMiddleware(videoSourceGetValidator), + getVideoSource +) + videosRouter.get('/:id', openapiOperationDoc({ operationId: 'getVideo' }), optionalAuthenticate, @@ -155,6 +164,10 @@ async function getVideoDescription (req: express.Request, res: express.Response) return res.json({ description }) } +function getVideoSource (req: express.Request, res: express.Response) { + return res.json(res.locals.videoSource.toFormattedJSON()) +} + async function listVideos (req: express.Request, res: express.Response) { const serverActor = await getServerActor() diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts index 3afbedbb2..c5890691e 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts @@ -44,6 +44,7 @@ import { import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' import { VideoModel } from '../../../models/video/video' import { VideoFileModel } from '../../../models/video/video-file' +import { VideoSourceModel } from '@server/models/video/video-source' const lTags = loggerTagsFactory('api', 'video') const auditLogger = auditLoggerFactory('videos') @@ -151,6 +152,7 @@ async function addVideo (options: { video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object const videoFile = await buildNewFile(videoPhysicalFile) + const originalFilename = videoPhysicalFile.originalname // Move physical file const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile) @@ -181,6 +183,11 @@ async function addVideo (options: { video.VideoFiles = [ videoFile ] + await VideoSourceModel.create({ + filename: originalFilename, + videoId: video.id + }, { transaction: t }) + await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) // Schedule an update in the future? diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index f54ce9506..0d7e7077d 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 710 +const LAST_MIGRATION_VERSION = 715 // --------------------------------------------------------------------------- diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 3576f444c..09786a91f 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -49,6 +49,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla import { VideoTagModel } from '../models/video/video-tag' import { VideoViewModel } from '../models/view/video-view' import { CONFIG } from './config' +import { VideoSourceModel } from '@server/models/video/video-source' require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string @@ -126,6 +127,7 @@ async function initDatabaseModels (silent: boolean) { VideoChannelModel, VideoShareModel, VideoFileModel, + VideoSourceModel, VideoCaptionModel, VideoBlacklistModel, VideoTagModel, diff --git a/server/initializers/migrations/0715-video-source.ts b/server/initializers/migrations/0715-video-source.ts new file mode 100644 index 000000000..efcf77ebd --- /dev/null +++ b/server/initializers/migrations/0715-video-source.ts @@ -0,0 +1,34 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction + queryInterface: Sequelize.QueryInterface + sequelize: Sequelize.Sequelize + db: any +}): Promise { + { + const query = ` + CREATE TABLE IF NOT EXISTS "videoSource" ( + "id" SERIAL , + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, + "filename" VARCHAR(255) DEFAULT NULL, + "videoId" INTEGER + REFERENCES "video" ("id") + ON DELETE CASCADE + ON UPDATE CASCADE, + PRIMARY KEY ("id") + ); + ` + await utils.sequelize.query(query) + } +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index bd2590bc5..1dd7b5d2e 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts @@ -9,6 +9,7 @@ export * from './video-ownership-changes' export * from './video-view' export * from './video-rates' export * from './video-shares' +export * from './video-source' export * from './video-stats' export * from './video-studio' export * from './video-transcoding' diff --git a/server/middlewares/validators/videos/video-source.ts b/server/middlewares/validators/videos/video-source.ts new file mode 100644 index 000000000..31a2f16b3 --- /dev/null +++ b/server/middlewares/validators/videos/video-source.ts @@ -0,0 +1,37 @@ +import express from 'express' +import { getVideoWithAttributes } from '@server/helpers/video' +import { VideoSourceModel } from '@server/models/video/video-source' +import { MVideoFullLight } from '@server/types/models' +import { HttpStatusCode, UserRight } from '@shared/models' +import { logger } from '../../../helpers/logger' +import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared' + +const videoSourceGetValidator = [ + isValidVideoIdParam('id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoSourceGet parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await doesVideoExist(req.params.id, res, 'for-api')) return + + const video = getVideoWithAttributes(res) as MVideoFullLight + + res.locals.videoSource = await VideoSourceModel.loadByVideoId(video.id) + if (!res.locals.videoSource) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video source not found' + }) + } + + const user = res.locals.oauth.token.User + if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return + + return next() + } +] + +export { + videoSourceGetValidator +} diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 0b6b8bfe5..c75c3640b 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts @@ -152,7 +152,7 @@ const videosAddResumableValidator = [ if (!await isVideoAccepted(req, res, file)) return cleanup() - res.locals.videoFileResumable = file + res.locals.videoFileResumable = { ...file, originalname: file.filename } return next() } diff --git a/server/models/video/video-source.ts b/server/models/video/video-source.ts new file mode 100644 index 000000000..e306b160d --- /dev/null +++ b/server/models/video/video-source.ts @@ -0,0 +1,55 @@ +import { Op } from 'sequelize' +import { + AllowNull, + BelongsTo, + Column, + CreatedAt, + ForeignKey, + Model, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { AttributesOnly } from '@shared/typescript-utils' +import { VideoModel } from './video' + +@Table({ + tableName: 'videoSource', + indexes: [ + { + fields: [ 'videoId' ], + where: { + videoId: { + [Op.ne]: null + } + } + } + ] +}) +export class VideoSourceModel extends Model>> { + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AllowNull(false) + @Column + filename: string + + @ForeignKey(() => VideoModel) + @Column + videoId: number + + @BelongsTo(() => VideoModel) + Video: VideoModel + + static loadByVideoId (videoId) { + return VideoSourceModel.findOne({ where: { videoId } }) + } + + toFormattedJSON () { + return { + filename: this.filename + } + } +} diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e6a8d3f95..08adbced6 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -136,6 +136,7 @@ import { VideoPlaylistElementModel } from './video-playlist-element' import { VideoShareModel } from './video-share' import { VideoStreamingPlaylistModel } from './video-streaming-playlist' import { VideoTagModel } from './video-tag' +import { VideoSourceModel } from './video-source' export enum ScopeNames { FOR_API = 'FOR_API', @@ -597,6 +598,15 @@ export class VideoModel extends Model>> { }) VideoPlaylistElements: VideoPlaylistElementModel[] + @HasOne(() => VideoSourceModel, { + foreignKey: { + name: 'videoId', + allowNull: true + }, + onDelete: 'CASCADE' + }) + VideoSource: VideoSourceModel + @HasMany(() => VideoAbuseModel, { foreignKey: { name: 'videoId', diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 259d7e783..a27bc8509 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts @@ -3,14 +3,14 @@ import './accounts' import './blocklist' import './bulk' import './config' -import './custom-pages' import './contact-form' +import './custom-pages' import './debug' import './follows' import './jobs' +import './live' import './logs' import './my-user' -import './live' import './plugins' import './redundancy' import './search' @@ -25,12 +25,13 @@ import './video-blacklist' import './video-captions' import './video-channels' import './video-comments' -import './video-studio' +import './video-files' import './video-imports' import './video-playlists' -import './videos' +import './video-source' +import './video-studio' import './videos-common-filters' -import './video-files' import './videos-history' import './videos-overviews' +import './videos' import './views' diff --git a/server/tests/api/check-params/video-source.ts b/server/tests/api/check-params/video-source.ts new file mode 100644 index 000000000..ca324bb9d --- /dev/null +++ b/server/tests/api/check-params/video-source.ts @@ -0,0 +1,44 @@ +import { HttpStatusCode } from '@shared/models' +import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' + +describe('Test video sources API validator', function () { + let server: PeerTubeServer = null + let uuid: string + let userToken: string + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + const created = await server.videos.quickUpload({ name: 'video' }) + uuid = created.uuid + + userToken = await server.users.generateUserAndToken('user') + }) + + it('Should fail without a valid uuid', async function () { + await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should receive 404 when passing a non existing video id', async function () { + await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should not get the source as unauthenticated', async function () { + await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null }) + }) + + it('Should not get the source with another user', async function () { + await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: userToken }) + }) + + it('Should succeed with the correct parameters get the source as another user', async function () { + await server.videos.getSource({ id: uuid }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index 27b119f30..a0b6b01cf 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts @@ -16,3 +16,4 @@ import './video-schedule-update' import './videos-common-filters' import './videos-history' import './videos-overview' +import './video-source' diff --git a/server/tests/api/videos/video-source.ts b/server/tests/api/videos/video-source.ts new file mode 100644 index 000000000..e34642300 --- /dev/null +++ b/server/tests/api/videos/video-source.ts @@ -0,0 +1,39 @@ +import 'mocha' +import * as chai from 'chai' +import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' + +const expect = chai.expect + +describe('Test video source', () => { + let server: PeerTubeServer = null + const fixture = 'video_short.webm' + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + }) + + it('Should get the source filename with legacy upload', async function () { + this.timeout(30000) + + const { uuid } = await server.videos.upload({ attributes: { name: 'my video', fixture }, mode: 'legacy' }) + + const source = await server.videos.getSource({ id: uuid }) + expect(source.filename).to.equal(fixture) + }) + + it('Should get the source filename with resumable upload', async function () { + this.timeout(30000) + + const { uuid } = await server.videos.upload({ attributes: { name: 'my video', fixture }, mode: 'resumable' }) + + const source = await server.videos.getSource({ id: uuid }) + expect(source.filename).to.equal(fixture) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/server/types/express.d.ts b/server/types/express.d.ts index 7cc13f21d..27e532c31 100644 --- a/server/types/express.d.ts +++ b/server/types/express.d.ts @@ -42,6 +42,7 @@ import { MVideoThumbnail } from './models' import { Writable } from 'stream' +import { MVideoSource } from './models/video/video-source' declare module 'express' { export interface Request { @@ -68,7 +69,7 @@ declare module 'express' { } | UploadFileForCheck[] // Upload file with a duration added by our middleware - export type VideoUploadFile = Pick & { + export type VideoUploadFile = Pick & { duration: number } @@ -85,6 +86,7 @@ declare module 'express' { duration: number path: string filename: string + originalname: string } // Extends Response with added functions and potential variables passed by middlewares @@ -123,6 +125,8 @@ declare module 'express' { videoShare?: MVideoShareActor + videoSource?: MVideoSource + videoFile?: MVideoFile videoFileResumable?: EnhancedUploadXFile diff --git a/server/types/models/video/video-source.ts b/server/types/models/video/video-source.ts new file mode 100644 index 000000000..0948f3b2e --- /dev/null +++ b/server/types/models/video/video-source.ts @@ -0,0 +1,3 @@ +import { VideoSourceModel } from '@server/models/video/video-source' + +export type MVideoSource = Omit -- cgit v1.2.3