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 <me@florianbigard.com>
---
 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<void> {
+  {
+    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<Partial<AttributesOnly<VideoSourceModel>>> {
+  @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<Partial<AttributesOnly<VideoModel>>> {
   })
   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<Express.Multer.File, 'path' | 'filename' | 'size'> & {
+  export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size', 'originalname'> & {
     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<VideoSourceModel, 'Video'>
-- 
cgit v1.2.3