diff options
Diffstat (limited to 'server/controllers/api/videos/update.ts')
-rw-r--r-- | server/controllers/api/videos/update.ts | 210 |
1 files changed, 0 insertions, 210 deletions
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts deleted file mode 100644 index 1edc509dc..000000000 --- a/server/controllers/api/videos/update.ts +++ /dev/null | |||
@@ -1,210 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { Transaction } from 'sequelize/types' | ||
3 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' | ||
4 | import { addVideoJobsAfterUpdate, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | ||
5 | import { setVideoPrivacy } from '@server/lib/video-privacy' | ||
6 | import { openapiOperationDoc } from '@server/middlewares/doc' | ||
7 | import { FilteredModelAttributes } from '@server/types' | ||
8 | import { MVideoFullLight } from '@server/types/models' | ||
9 | import { forceNumber } from '@shared/core-utils' | ||
10 | import { HttpStatusCode, VideoPrivacy, VideoUpdate } from '@shared/models' | ||
11 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | ||
12 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | ||
13 | import { createReqFiles } from '../../../helpers/express-utils' | ||
14 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
15 | import { MIMETYPES } from '../../../initializers/constants' | ||
16 | import { sequelizeTypescript } from '../../../initializers/database' | ||
17 | import { Hooks } from '../../../lib/plugins/hooks' | ||
18 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | ||
19 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' | ||
20 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | ||
21 | import { VideoModel } from '../../../models/video/video' | ||
22 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
23 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
24 | import { exists } from '@server/helpers/custom-validators/misc' | ||
25 | |||
26 | const lTags = loggerTagsFactory('api', 'video') | ||
27 | const auditLogger = auditLoggerFactory('videos') | ||
28 | const updateRouter = express.Router() | ||
29 | |||
30 | const reqVideoFileUpdate = createReqFiles([ 'thumbnailfile', 'previewfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) | ||
31 | |||
32 | updateRouter.put('/:id', | ||
33 | openapiOperationDoc({ operationId: 'putVideo' }), | ||
34 | authenticate, | ||
35 | reqVideoFileUpdate, | ||
36 | asyncMiddleware(videosUpdateValidator), | ||
37 | asyncRetryTransactionMiddleware(updateVideo) | ||
38 | ) | ||
39 | |||
40 | // --------------------------------------------------------------------------- | ||
41 | |||
42 | export { | ||
43 | updateRouter | ||
44 | } | ||
45 | |||
46 | // --------------------------------------------------------------------------- | ||
47 | |||
48 | async function updateVideo (req: express.Request, res: express.Response) { | ||
49 | const videoFromReq = res.locals.videoAll | ||
50 | const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON()) | ||
51 | const videoInfoToUpdate: VideoUpdate = req.body | ||
52 | |||
53 | const hadPrivacyForFederation = videoFromReq.hasPrivacyForFederation() | ||
54 | const oldPrivacy = videoFromReq.privacy | ||
55 | |||
56 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
57 | video: videoFromReq, | ||
58 | files: req.files, | ||
59 | fallback: () => Promise.resolve(undefined), | ||
60 | automaticallyGenerated: false | ||
61 | }) | ||
62 | |||
63 | const videoFileLockReleaser = await VideoPathManager.Instance.lockFiles(videoFromReq.uuid) | ||
64 | |||
65 | try { | ||
66 | const { videoInstanceUpdated, isNewVideo } = await sequelizeTypescript.transaction(async t => { | ||
67 | // Refresh video since thumbnails to prevent concurrent updates | ||
68 | const video = await VideoModel.loadFull(videoFromReq.id, t) | ||
69 | |||
70 | const oldVideoChannel = video.VideoChannel | ||
71 | |||
72 | const keysToUpdate: (keyof VideoUpdate & FilteredModelAttributes<VideoModel>)[] = [ | ||
73 | 'name', | ||
74 | 'category', | ||
75 | 'licence', | ||
76 | 'language', | ||
77 | 'nsfw', | ||
78 | 'waitTranscoding', | ||
79 | 'support', | ||
80 | 'description', | ||
81 | 'commentsEnabled', | ||
82 | 'downloadEnabled' | ||
83 | ] | ||
84 | |||
85 | for (const key of keysToUpdate) { | ||
86 | if (videoInfoToUpdate[key] !== undefined) video.set(key, videoInfoToUpdate[key]) | ||
87 | } | ||
88 | |||
89 | if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) { | ||
90 | video.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt) | ||
91 | } | ||
92 | |||
93 | // Privacy update? | ||
94 | let isNewVideo = false | ||
95 | if (videoInfoToUpdate.privacy !== undefined) { | ||
96 | isNewVideo = await updateVideoPrivacy({ videoInstance: video, videoInfoToUpdate, hadPrivacyForFederation, transaction: t }) | ||
97 | } | ||
98 | |||
99 | // Force updatedAt attribute change | ||
100 | if (!video.changed()) { | ||
101 | await video.setAsRefreshed(t) | ||
102 | } | ||
103 | |||
104 | const videoInstanceUpdated = await video.save({ transaction: t }) as MVideoFullLight | ||
105 | |||
106 | // Thumbnail & preview updates? | ||
107 | if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t) | ||
108 | if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t) | ||
109 | |||
110 | // Video tags update? | ||
111 | if (videoInfoToUpdate.tags !== undefined) { | ||
112 | await setVideoTags({ video: videoInstanceUpdated, tags: videoInfoToUpdate.tags, transaction: t }) | ||
113 | } | ||
114 | |||
115 | // Video channel update? | ||
116 | if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) { | ||
117 | await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) | ||
118 | videoInstanceUpdated.VideoChannel = res.locals.videoChannel | ||
119 | |||
120 | if (hadPrivacyForFederation === true) { | ||
121 | await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) | ||
122 | } | ||
123 | } | ||
124 | |||
125 | // Schedule an update in the future? | ||
126 | await updateSchedule(videoInstanceUpdated, videoInfoToUpdate, t) | ||
127 | |||
128 | await autoBlacklistVideoIfNeeded({ | ||
129 | video: videoInstanceUpdated, | ||
130 | user: res.locals.oauth.token.User, | ||
131 | isRemote: false, | ||
132 | isNew: false, | ||
133 | isNewFile: false, | ||
134 | transaction: t | ||
135 | }) | ||
136 | |||
137 | auditLogger.update( | ||
138 | getAuditIdFromRes(res), | ||
139 | new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), | ||
140 | oldVideoAuditView | ||
141 | ) | ||
142 | logger.info('Video with name %s and uuid %s updated.', video.name, video.uuid, lTags(video.uuid)) | ||
143 | |||
144 | return { videoInstanceUpdated, isNewVideo } | ||
145 | }) | ||
146 | |||
147 | Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body, req, res }) | ||
148 | |||
149 | await addVideoJobsAfterUpdate({ | ||
150 | video: videoInstanceUpdated, | ||
151 | nameChanged: !!videoInfoToUpdate.name, | ||
152 | oldPrivacy, | ||
153 | isNewVideo | ||
154 | }) | ||
155 | } catch (err) { | ||
156 | // If the transaction is retried, sequelize will think the object has not changed | ||
157 | // So we need to restore the previous fields | ||
158 | await resetSequelizeInstance(videoFromReq) | ||
159 | |||
160 | throw err | ||
161 | } finally { | ||
162 | videoFileLockReleaser() | ||
163 | } | ||
164 | |||
165 | return res.type('json') | ||
166 | .status(HttpStatusCode.NO_CONTENT_204) | ||
167 | .end() | ||
168 | } | ||
169 | |||
170 | async function updateVideoPrivacy (options: { | ||
171 | videoInstance: MVideoFullLight | ||
172 | videoInfoToUpdate: VideoUpdate | ||
173 | hadPrivacyForFederation: boolean | ||
174 | transaction: Transaction | ||
175 | }) { | ||
176 | const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options | ||
177 | const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy) | ||
178 | |||
179 | const newPrivacy = forceNumber(videoInfoToUpdate.privacy) | ||
180 | setVideoPrivacy(videoInstance, newPrivacy) | ||
181 | |||
182 | // Delete passwords if video is not anymore password protected | ||
183 | if (videoInstance.privacy === VideoPrivacy.PASSWORD_PROTECTED && newPrivacy !== VideoPrivacy.PASSWORD_PROTECTED) { | ||
184 | await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction) | ||
185 | } | ||
186 | |||
187 | if (newPrivacy === VideoPrivacy.PASSWORD_PROTECTED && exists(videoInfoToUpdate.videoPasswords)) { | ||
188 | await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction) | ||
189 | await VideoPasswordModel.addPasswords(videoInfoToUpdate.videoPasswords, videoInstance.id, transaction) | ||
190 | } | ||
191 | |||
192 | // Unfederate the video if the new privacy is not compatible with federation | ||
193 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { | ||
194 | await VideoModel.sendDelete(videoInstance, { transaction }) | ||
195 | } | ||
196 | |||
197 | return isNewVideo | ||
198 | } | ||
199 | |||
200 | function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: VideoUpdate, transaction: Transaction) { | ||
201 | if (videoInfoToUpdate.scheduleUpdate) { | ||
202 | return ScheduleVideoUpdateModel.upsert({ | ||
203 | videoId: videoInstance.id, | ||
204 | updateAt: new Date(videoInfoToUpdate.scheduleUpdate.updateAt), | ||
205 | privacy: videoInfoToUpdate.scheduleUpdate.privacy || null | ||
206 | }, { transaction }) | ||
207 | } else if (videoInfoToUpdate.scheduleUpdate === null) { | ||
208 | return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction) | ||
209 | } | ||
210 | } | ||