diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-19 16:02:49 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-07-21 17:38:13 +0200 |
commit | 12dc3a942a13c7f1489822dae052da197ef15905 (patch) | |
tree | 7b87b6be692af0b62ebac17e720c80244fd8a7ec /server/controllers | |
parent | c6867725fb8e3dfbc2018a37ed5a963103587cb6 (diff) | |
download | PeerTube-12dc3a942a13c7f1489822dae052da197ef15905.tar.gz PeerTube-12dc3a942a13c7f1489822dae052da197ef15905.tar.zst PeerTube-12dc3a942a13c7f1489822dae052da197ef15905.zip |
Implement replace file in server side
Diffstat (limited to 'server/controllers')
-rw-r--r-- | server/controllers/api/config.ts | 5 | ||||
-rw-r--r-- | server/controllers/api/videos/index.ts | 16 | ||||
-rw-r--r-- | server/controllers/api/videos/source.ts | 206 | ||||
-rw-r--r-- | server/controllers/api/videos/update.ts | 1 | ||||
-rw-r--r-- | server/controllers/api/videos/upload.ts | 11 |
5 files changed, 221 insertions, 18 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 0980ec10a..c5c4c8a74 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -284,6 +284,11 @@ function customConfig (): CustomConfig { | |||
284 | enabled: CONFIG.VIDEO_STUDIO.REMOTE_RUNNERS.ENABLED | 284 | enabled: CONFIG.VIDEO_STUDIO.REMOTE_RUNNERS.ENABLED |
285 | } | 285 | } |
286 | }, | 286 | }, |
287 | videoFile: { | ||
288 | update: { | ||
289 | enabled: CONFIG.VIDEO_FILE.UPDATE.ENABLED | ||
290 | } | ||
291 | }, | ||
287 | import: { | 292 | import: { |
288 | videos: { | 293 | videos: { |
289 | concurrency: CONFIG.IMPORT.VIDEOS.CONCURRENCY, | 294 | concurrency: CONFIG.IMPORT.VIDEOS.CONCURRENCY, |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 520d8cbbb..3cdd42289 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -26,7 +26,6 @@ import { | |||
26 | setDefaultVideosSort, | 26 | setDefaultVideosSort, |
27 | videosCustomGetValidator, | 27 | videosCustomGetValidator, |
28 | videosGetValidator, | 28 | videosGetValidator, |
29 | videoSourceGetValidator, | ||
30 | videosRemoveValidator, | 29 | videosRemoveValidator, |
31 | videosSortValidator | 30 | videosSortValidator |
32 | } from '../../../middlewares' | 31 | } from '../../../middlewares' |
@@ -39,7 +38,9 @@ import { filesRouter } from './files' | |||
39 | import { videoImportsRouter } from './import' | 38 | import { videoImportsRouter } from './import' |
40 | import { liveRouter } from './live' | 39 | import { liveRouter } from './live' |
41 | import { ownershipVideoRouter } from './ownership' | 40 | import { ownershipVideoRouter } from './ownership' |
41 | import { videoPasswordRouter } from './passwords' | ||
42 | import { rateVideoRouter } from './rate' | 42 | import { rateVideoRouter } from './rate' |
43 | import { videoSourceRouter } from './source' | ||
43 | import { statsRouter } from './stats' | 44 | import { statsRouter } from './stats' |
44 | import { storyboardRouter } from './storyboard' | 45 | import { storyboardRouter } from './storyboard' |
45 | import { studioRouter } from './studio' | 46 | import { studioRouter } from './studio' |
@@ -48,7 +49,6 @@ import { transcodingRouter } from './transcoding' | |||
48 | import { updateRouter } from './update' | 49 | import { updateRouter } from './update' |
49 | import { uploadRouter } from './upload' | 50 | import { uploadRouter } from './upload' |
50 | import { viewRouter } from './view' | 51 | import { viewRouter } from './view' |
51 | import { videoPasswordRouter } from './passwords' | ||
52 | 52 | ||
53 | const auditLogger = auditLoggerFactory('videos') | 53 | const auditLogger = auditLoggerFactory('videos') |
54 | const videosRouter = express.Router() | 54 | const videosRouter = express.Router() |
@@ -72,6 +72,7 @@ videosRouter.use('/', transcodingRouter) | |||
72 | videosRouter.use('/', tokenRouter) | 72 | videosRouter.use('/', tokenRouter) |
73 | videosRouter.use('/', videoPasswordRouter) | 73 | videosRouter.use('/', videoPasswordRouter) |
74 | videosRouter.use('/', storyboardRouter) | 74 | videosRouter.use('/', storyboardRouter) |
75 | videosRouter.use('/', videoSourceRouter) | ||
75 | 76 | ||
76 | videosRouter.get('/categories', | 77 | videosRouter.get('/categories', |
77 | openapiOperationDoc({ operationId: 'getCategories' }), | 78 | openapiOperationDoc({ operationId: 'getCategories' }), |
@@ -108,13 +109,6 @@ videosRouter.get('/:id/description', | |||
108 | asyncMiddleware(getVideoDescription) | 109 | asyncMiddleware(getVideoDescription) |
109 | ) | 110 | ) |
110 | 111 | ||
111 | videosRouter.get('/:id/source', | ||
112 | openapiOperationDoc({ operationId: 'getVideoSource' }), | ||
113 | authenticate, | ||
114 | asyncMiddleware(videoSourceGetValidator), | ||
115 | getVideoSource | ||
116 | ) | ||
117 | |||
118 | videosRouter.get('/:id', | 112 | videosRouter.get('/:id', |
119 | openapiOperationDoc({ operationId: 'getVideo' }), | 113 | openapiOperationDoc({ operationId: 'getVideo' }), |
120 | optionalAuthenticate, | 114 | optionalAuthenticate, |
@@ -177,10 +171,6 @@ async function getVideoDescription (req: express.Request, res: express.Response) | |||
177 | return res.json({ description }) | 171 | return res.json({ description }) |
178 | } | 172 | } |
179 | 173 | ||
180 | function getVideoSource (req: express.Request, res: express.Response) { | ||
181 | return res.json(res.locals.videoSource.toFormattedJSON()) | ||
182 | } | ||
183 | |||
184 | async function listVideos (req: express.Request, res: express.Response) { | 174 | async function listVideos (req: express.Request, res: express.Response) { |
185 | const serverActor = await getServerActor() | 175 | const serverActor = await getServerActor() |
186 | 176 | ||
diff --git a/server/controllers/api/videos/source.ts b/server/controllers/api/videos/source.ts new file mode 100644 index 000000000..b20c4af0e --- /dev/null +++ b/server/controllers/api/videos/source.ts | |||
@@ -0,0 +1,206 @@ | |||
1 | import express from 'express' | ||
2 | import { move } from 'fs-extra' | ||
3 | import { sequelizeTypescript } from '@server/initializers/database' | ||
4 | import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue' | ||
5 | import { Hooks } from '@server/lib/plugins/hooks' | ||
6 | import { regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail' | ||
7 | import { uploadx } from '@server/lib/uploadx' | ||
8 | import { buildMoveToObjectStorageJob } from '@server/lib/video' | ||
9 | import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' | ||
10 | import { buildNewFile } from '@server/lib/video-file' | ||
11 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
12 | import { buildNextVideoState } from '@server/lib/video-state' | ||
13 | import { openapiOperationDoc } from '@server/middlewares/doc' | ||
14 | import { VideoModel } from '@server/models/video/video' | ||
15 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
16 | import { MStreamingPlaylistFiles, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | ||
17 | import { HttpStatusCode, VideoState } from '@shared/models' | ||
18 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
19 | import { | ||
20 | asyncMiddleware, | ||
21 | authenticate, | ||
22 | replaceVideoSourceResumableInitValidator, | ||
23 | replaceVideoSourceResumableValidator, | ||
24 | videoSourceGetLatestValidator | ||
25 | } from '../../../middlewares' | ||
26 | |||
27 | const lTags = loggerTagsFactory('api', 'video') | ||
28 | |||
29 | const videoSourceRouter = express.Router() | ||
30 | |||
31 | videoSourceRouter.get('/:id/source', | ||
32 | openapiOperationDoc({ operationId: 'getVideoSource' }), | ||
33 | authenticate, | ||
34 | asyncMiddleware(videoSourceGetLatestValidator), | ||
35 | getVideoLatestSource | ||
36 | ) | ||
37 | |||
38 | videoSourceRouter.post('/:id/source/replace-resumable', | ||
39 | authenticate, | ||
40 | asyncMiddleware(replaceVideoSourceResumableInitValidator), | ||
41 | (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end | ||
42 | ) | ||
43 | |||
44 | videoSourceRouter.delete('/:id/source/replace-resumable', | ||
45 | authenticate, | ||
46 | (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end | ||
47 | ) | ||
48 | |||
49 | videoSourceRouter.put('/:id/source/replace-resumable', | ||
50 | authenticate, | ||
51 | uploadx.upload, // uploadx doesn't next() before the file upload completes | ||
52 | asyncMiddleware(replaceVideoSourceResumableValidator), | ||
53 | asyncMiddleware(replaceVideoSourceResumable) | ||
54 | ) | ||
55 | |||
56 | // --------------------------------------------------------------------------- | ||
57 | |||
58 | export { | ||
59 | videoSourceRouter | ||
60 | } | ||
61 | |||
62 | // --------------------------------------------------------------------------- | ||
63 | |||
64 | function getVideoLatestSource (req: express.Request, res: express.Response) { | ||
65 | return res.json(res.locals.videoSource.toFormattedJSON()) | ||
66 | } | ||
67 | |||
68 | async function replaceVideoSourceResumable (req: express.Request, res: express.Response) { | ||
69 | const videoPhysicalFile = res.locals.updateVideoFileResumable | ||
70 | const user = res.locals.oauth.token.User | ||
71 | |||
72 | const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video' }) | ||
73 | const originalFilename = videoPhysicalFile.originalname | ||
74 | |||
75 | const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(res.locals.videoAll.uuid) | ||
76 | |||
77 | try { | ||
78 | const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(res.locals.videoAll, videoFile) | ||
79 | await move(videoPhysicalFile.path, destination) | ||
80 | |||
81 | let oldWebVideoFiles: MVideoFile[] = [] | ||
82 | let oldStreamingPlaylists: MStreamingPlaylistFiles[] = [] | ||
83 | |||
84 | const inputFileUpdatedAt = new Date() | ||
85 | |||
86 | const video = await sequelizeTypescript.transaction(async transaction => { | ||
87 | const video = await VideoModel.loadFull(res.locals.videoAll.id, transaction) | ||
88 | |||
89 | oldWebVideoFiles = video.VideoFiles | ||
90 | oldStreamingPlaylists = video.VideoStreamingPlaylists | ||
91 | |||
92 | for (const file of video.VideoFiles) { | ||
93 | await file.destroy({ transaction }) | ||
94 | } | ||
95 | for (const playlist of oldStreamingPlaylists) { | ||
96 | await playlist.destroy({ transaction }) | ||
97 | } | ||
98 | |||
99 | videoFile.videoId = video.id | ||
100 | await videoFile.save({ transaction }) | ||
101 | |||
102 | video.VideoFiles = [ videoFile ] | ||
103 | video.VideoStreamingPlaylists = [] | ||
104 | |||
105 | video.state = buildNextVideoState() | ||
106 | video.duration = videoPhysicalFile.duration | ||
107 | video.inputFileUpdatedAt = inputFileUpdatedAt | ||
108 | await video.save({ transaction }) | ||
109 | |||
110 | await autoBlacklistVideoIfNeeded({ | ||
111 | video, | ||
112 | user, | ||
113 | isRemote: false, | ||
114 | isNew: false, | ||
115 | isNewFile: true, | ||
116 | transaction | ||
117 | }) | ||
118 | |||
119 | return video | ||
120 | }) | ||
121 | |||
122 | await removeOldFiles({ video, files: oldWebVideoFiles, playlists: oldStreamingPlaylists }) | ||
123 | |||
124 | await VideoSourceModel.create({ | ||
125 | filename: originalFilename, | ||
126 | videoId: video.id, | ||
127 | createdAt: inputFileUpdatedAt | ||
128 | }) | ||
129 | |||
130 | await regenerateMiniaturesIfNeeded(video) | ||
131 | await video.VideoChannel.setAsUpdated() | ||
132 | await addVideoJobsAfterUpload(video, video.getMaxQualityFile()) | ||
133 | |||
134 | logger.info('Replaced video file of video %s with uuid %s.', video.name, video.uuid, lTags(video.uuid)) | ||
135 | |||
136 | Hooks.runAction('action:api.video.file-updated', { video, req, res }) | ||
137 | |||
138 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
139 | } finally { | ||
140 | videoFileMutexReleaser() | ||
141 | } | ||
142 | } | ||
143 | |||
144 | async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile) { | ||
145 | const jobs: (CreateJobArgument & CreateJobOptions)[] = [ | ||
146 | { | ||
147 | type: 'manage-video-torrent' as 'manage-video-torrent', | ||
148 | payload: { | ||
149 | videoId: video.id, | ||
150 | videoFileId: videoFile.id, | ||
151 | action: 'create' | ||
152 | } | ||
153 | }, | ||
154 | |||
155 | { | ||
156 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
157 | payload: { | ||
158 | videoUUID: video.uuid, | ||
159 | // No need to federate, we process these jobs sequentially | ||
160 | federate: false | ||
161 | } | ||
162 | }, | ||
163 | |||
164 | { | ||
165 | type: 'federate-video' as 'federate-video', | ||
166 | payload: { | ||
167 | videoUUID: video.uuid, | ||
168 | isNewVideo: false | ||
169 | } | ||
170 | } | ||
171 | ] | ||
172 | |||
173 | if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { | ||
174 | jobs.push(await buildMoveToObjectStorageJob({ video, isNewVideo: false, previousVideoState: undefined })) | ||
175 | } | ||
176 | |||
177 | if (video.state === VideoState.TO_TRANSCODE) { | ||
178 | jobs.push({ | ||
179 | type: 'transcoding-job-builder' as 'transcoding-job-builder', | ||
180 | payload: { | ||
181 | videoUUID: video.uuid, | ||
182 | optimizeJob: { | ||
183 | isNewVideo: false | ||
184 | } | ||
185 | } | ||
186 | }) | ||
187 | } | ||
188 | |||
189 | return JobQueue.Instance.createSequentialJobFlow(...jobs) | ||
190 | } | ||
191 | |||
192 | async function removeOldFiles (options: { | ||
193 | video: MVideo | ||
194 | files: MVideoFile[] | ||
195 | playlists: MStreamingPlaylistFiles[] | ||
196 | }) { | ||
197 | const { video, files, playlists } = options | ||
198 | |||
199 | for (const file of files) { | ||
200 | await video.removeWebVideoFile(file) | ||
201 | } | ||
202 | |||
203 | for (const playlist of playlists) { | ||
204 | await video.removeStreamingPlaylistFiles(playlist) | ||
205 | } | ||
206 | } | ||
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts index 28ec2cf37..1edc509dc 100644 --- a/server/controllers/api/videos/update.ts +++ b/server/controllers/api/videos/update.ts | |||
@@ -130,6 +130,7 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
130 | user: res.locals.oauth.token.User, | 130 | user: res.locals.oauth.token.User, |
131 | isRemote: false, | 131 | isRemote: false, |
132 | isNew: false, | 132 | isNew: false, |
133 | isNewFile: false, | ||
133 | transaction: t | 134 | transaction: t |
134 | }) | 135 | }) |
135 | 136 | ||
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts index 27fef0b1a..e520bf4b5 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts | |||
@@ -11,8 +11,9 @@ import { buildNewFile } from '@server/lib/video-file' | |||
11 | import { VideoPathManager } from '@server/lib/video-path-manager' | 11 | import { VideoPathManager } from '@server/lib/video-path-manager' |
12 | import { buildNextVideoState } from '@server/lib/video-state' | 12 | import { buildNextVideoState } from '@server/lib/video-state' |
13 | import { openapiOperationDoc } from '@server/middlewares/doc' | 13 | import { openapiOperationDoc } from '@server/middlewares/doc' |
14 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
14 | import { VideoSourceModel } from '@server/models/video/video-source' | 15 | import { VideoSourceModel } from '@server/models/video/video-source' |
15 | import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models' | 16 | import { MVideoFile, MVideoFullLight } from '@server/types/models' |
16 | import { uuidToShort } from '@shared/extra-utils' | 17 | import { uuidToShort } from '@shared/extra-utils' |
17 | import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' | 18 | import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' |
18 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 19 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
@@ -33,7 +34,6 @@ import { | |||
33 | } from '../../../middlewares' | 34 | } from '../../../middlewares' |
34 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 35 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
35 | import { VideoModel } from '../../../models/video/video' | 36 | import { VideoModel } from '../../../models/video/video' |
36 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
37 | 37 | ||
38 | const lTags = loggerTagsFactory('api', 'video') | 38 | const lTags = loggerTagsFactory('api', 'video') |
39 | const auditLogger = auditLoggerFactory('videos') | 39 | const auditLogger = auditLoggerFactory('videos') |
@@ -109,7 +109,7 @@ async function addVideoLegacy (req: express.Request, res: express.Response) { | |||
109 | } | 109 | } |
110 | 110 | ||
111 | async function addVideoResumable (req: express.Request, res: express.Response) { | 111 | async function addVideoResumable (req: express.Request, res: express.Response) { |
112 | const videoPhysicalFile = res.locals.videoFileResumable | 112 | const videoPhysicalFile = res.locals.uploadVideoFileResumable |
113 | const videoInfo = videoPhysicalFile.metadata | 113 | const videoInfo = videoPhysicalFile.metadata |
114 | const files = { previewfile: videoInfo.previewfile, thumbnailfile: videoInfo.thumbnailfile } | 114 | const files = { previewfile: videoInfo.previewfile, thumbnailfile: videoInfo.thumbnailfile } |
115 | 115 | ||
@@ -193,6 +193,7 @@ async function addVideo (options: { | |||
193 | user, | 193 | user, |
194 | isRemote: false, | 194 | isRemote: false, |
195 | isNew: true, | 195 | isNew: true, |
196 | isNewFile: true, | ||
196 | transaction: t | 197 | transaction: t |
197 | }) | 198 | }) |
198 | 199 | ||
@@ -209,7 +210,7 @@ async function addVideo (options: { | |||
209 | // Channel has a new content, set as updated | 210 | // Channel has a new content, set as updated |
210 | await videoCreated.VideoChannel.setAsUpdated() | 211 | await videoCreated.VideoChannel.setAsUpdated() |
211 | 212 | ||
212 | addVideoJobsAfterUpload(videoCreated, videoFile, user) | 213 | addVideoJobsAfterUpload(videoCreated, videoFile) |
213 | .catch(err => logger.error('Cannot build new video jobs of %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) })) | 214 | .catch(err => logger.error('Cannot build new video jobs of %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) })) |
214 | 215 | ||
215 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated, req, res }) | 216 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated, req, res }) |
@@ -223,7 +224,7 @@ async function addVideo (options: { | |||
223 | } | 224 | } |
224 | } | 225 | } |
225 | 226 | ||
226 | async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile, user: MUserId) { | 227 | async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile) { |
227 | const jobs: (CreateJobArgument & CreateJobOptions)[] = [ | 228 | const jobs: (CreateJobArgument & CreateJobOptions)[] = [ |
228 | { | 229 | { |
229 | type: 'manage-video-torrent' as 'manage-video-torrent', | 230 | type: 'manage-video-torrent' as 'manage-video-torrent', |