Unverified Commit 2e401e85 authored by kontrollanten's avatar kontrollanten Committed by GitHub
Browse files

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's avatarChocobozzz <me@florianbigard.com>
parent dec49521
......@@ -340,6 +340,21 @@
</div>
<div class="col-md-12 col-xl-4">
<div *ngIf="videoSource" class="form-group">
<label i18n for="filename">Filename</label>
<my-help>
<ng-template ptTemplate="preHtml">
<ng-container i18n>
Name of the uploaded file
</ng-container>
</ng-template>
</my-help>
<input type="text" [disabled]="true" id="filename" class="form-control" [value]="videoSource.filename" />
</div>
<div class="form-group originally-published-at">
<label i18n for="originallyPublishedAt">Original publication date</label>
<my-help>
......
......@@ -37,6 +37,7 @@ import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
import { VideoEditType } from './video-edit.type'
import { VideoSource } from '@shared/models/videos/video-source'
type VideoLanguages = VideoConstant<string> & { group?: string }
type PluginField = {
......@@ -61,6 +62,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
@Input() forbidScheduledPublication = true
@Input() videoCaptions: VideoCaptionWithPathEdit[] = []
@Input() videoSource: VideoSource
@Input() waitTranscodingEnabled = true
@Input() type: VideoEditType
......
......@@ -12,6 +12,7 @@
[videoCaptions]="videoCaptions" [waitTranscodingEnabled]="isWaitTranscodingEnabled()"
type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
[liveVideo]="liveVideo" [videoToUpdate]="videoDetails"
[videoSource]="videoSource"
(formBuilt)="onFormBuilt()"
></my-video-edit>
......
......@@ -10,6 +10,7 @@ import { LiveVideoService } from '@app/shared/shared-video-live'
import { LoadingBarService } from '@ngx-loading-bar/core'
import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models'
import { hydrateFormFromVideo } from './shared/video-edit-utils'
import { VideoSource } from '@shared/models/videos/video-source'
@Component({
selector: 'my-videos-update',
......@@ -19,6 +20,7 @@ import { hydrateFormFromVideo } from './shared/video-edit-utils'
export class VideoUpdateComponent extends FormReactive implements OnInit {
video: VideoEdit
videoDetails: VideoDetails
videoSource: VideoSource
userVideoChannels: SelectChannelItem[] = []
videoCaptions: VideoCaptionEdit[] = []
liveVideo: LiveVideo
......@@ -46,13 +48,14 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
this.buildForm({})
const { videoData } = this.route.snapshot.data
const { video, videoChannels, videoCaptions, liveVideo } = videoData
const { video, videoChannels, videoCaptions, videoSource, liveVideo } = videoData
this.video = new VideoEdit(video)
this.videoDetails = video
this.userVideoChannels = videoChannels
this.videoCaptions = videoCaptions
this.videoSource = videoSource
this.liveVideo = liveVideo
this.forbidScheduledPublication = this.video.privacy !== VideoPrivacy.PRIVATE
......
......@@ -23,7 +23,8 @@ export class VideoUpdateResolver implements Resolve<any> {
return this.videoService.getVideo({ videoId: uuid })
.pipe(
switchMap(video => forkJoin(this.buildVideoObservables(video))),
map(([ video, videoChannels, videoCaptions, liveVideo ]) => ({ video, videoChannels, videoCaptions, liveVideo }))
map(([ video, videoSource, videoChannels, videoCaptions, liveVideo ]) =>
({ video, videoChannels, videoCaptions, videoSource, liveVideo }))
)
}
......@@ -33,6 +34,8 @@ export class VideoUpdateResolver implements Resolve<any> {
.loadCompleteDescription(video.descriptionPath)
.pipe(map(description => Object.assign(video, { description }))),
this.videoService.getSource(video.id),
listUserChannelsForSelect(this.authService),
this.videoCaptionService
......
import { SortMeta } from 'primeng/api'
import { from, Observable } from 'rxjs'
import { from, Observable, of } from 'rxjs'
import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
import { Injectable } from '@angular/core'
......@@ -24,6 +24,7 @@ import {
VideoTranscodingCreate,
VideoUpdate
} from '@shared/models'
import { VideoSource } from '@shared/models/videos/video-source'
import { environment } from '../../../../environments/environment'
import { Account } from '../account/account.model'
import { AccountService } from '../account/account.service'
......@@ -323,6 +324,20 @@ export class VideoService {
)
}
getSource (videoId: number) {
return this.authHttp
.get<{ source: VideoSource }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source')
.pipe(
catchError(err => {
if (err.status === 404) {
return of(undefined)
}
this.restExtractor.handleError(err)
})
)
}
setVideoLike (id: number) {
return this.setVideoRate(id, 'like')
}
......
......@@ -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()
......
......@@ -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?
......
......@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 710
const LAST_MIGRATION_VERSION = 715
// ---------------------------------------------------------------------------
......
......@@ -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,
......
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
}
......@@ -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'
......
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
}
......@@ -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()
}
......
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
}
}
}
......@@ -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',
......
......@@ -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'
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 ])
})
})
......@@ -16,3 +16,4 @@ import './video-schedule-update'
import './videos-common-filters'
import './videos-history'
import './videos-overview'
import './video-source'
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 ])
})
})
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment