aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/redundancy
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/redundancy')
-rw-r--r--server/models/redundancy/video-redundancy.ts192
1 files changed, 131 insertions, 61 deletions
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 8f2ef2d9a..eb2222256 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -13,9 +13,9 @@ import {
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { ActorModel } from '../activitypub/actor' 15import { ActorModel } from '../activitypub/actor'
16import { getVideoSort, throwIfNotValid } from '../utils' 16import { getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' 17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { CONFIG, CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers' 18import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
19import { VideoFileModel } from '../video/video-file' 19import { VideoFileModel } from '../video/video-file'
20import { getServerActor } from '../../helpers/utils' 20import { getServerActor } from '../../helpers/utils'
21import { VideoModel } from '../video/video' 21import { VideoModel } from '../video/video'
@@ -27,28 +27,40 @@ import { ServerModel } from '../server/server'
27import { sample } from 'lodash' 27import { sample } from 'lodash'
28import { isTestInstance } from '../../helpers/core-utils' 28import { isTestInstance } from '../../helpers/core-utils'
29import * as Bluebird from 'bluebird' 29import * as Bluebird from 'bluebird'
30import * as Sequelize from 'sequelize' 30import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize'
31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
32import { CONFIG } from '../../initializers/config'
31 33
32export enum ScopeNames { 34export enum ScopeNames {
33 WITH_VIDEO = 'WITH_VIDEO' 35 WITH_VIDEO = 'WITH_VIDEO'
34} 36}
35 37
36@Scopes({ 38@Scopes(() => ({
37 [ ScopeNames.WITH_VIDEO ]: { 39 [ ScopeNames.WITH_VIDEO ]: {
38 include: [ 40 include: [
39 { 41 {
40 model: () => VideoFileModel, 42 model: VideoFileModel,
41 required: true, 43 required: false,
42 include: [ 44 include: [
43 { 45 {
44 model: () => VideoModel, 46 model: VideoModel,
47 required: true
48 }
49 ]
50 },
51 {
52 model: VideoStreamingPlaylistModel,
53 required: false,
54 include: [
55 {
56 model: VideoModel,
45 required: true 57 required: true
46 } 58 }
47 ] 59 ]
48 } 60 }
49 ] 61 ]
50 } 62 }
51}) 63}))
52 64
53@Table({ 65@Table({
54 tableName: 'videoRedundancy', 66 tableName: 'videoRedundancy',
@@ -97,12 +109,24 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
97 109
98 @BelongsTo(() => VideoFileModel, { 110 @BelongsTo(() => VideoFileModel, {
99 foreignKey: { 111 foreignKey: {
100 allowNull: false 112 allowNull: true
101 }, 113 },
102 onDelete: 'cascade' 114 onDelete: 'cascade'
103 }) 115 })
104 VideoFile: VideoFileModel 116 VideoFile: VideoFileModel
105 117
118 @ForeignKey(() => VideoStreamingPlaylistModel)
119 @Column
120 videoStreamingPlaylistId: number
121
122 @BelongsTo(() => VideoStreamingPlaylistModel, {
123 foreignKey: {
124 allowNull: true
125 },
126 onDelete: 'cascade'
127 })
128 VideoStreamingPlaylist: VideoStreamingPlaylistModel
129
106 @ForeignKey(() => ActorModel) 130 @ForeignKey(() => ActorModel)
107 @Column 131 @Column
108 actorId: number 132 actorId: number
@@ -119,13 +143,25 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
119 static async removeFile (instance: VideoRedundancyModel) { 143 static async removeFile (instance: VideoRedundancyModel) {
120 if (!instance.isOwned()) return 144 if (!instance.isOwned()) return
121 145
122 const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) 146 if (instance.videoFileId) {
147 const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
123 148
124 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` 149 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
125 logger.info('Removing duplicated video file %s.', logIdentifier) 150 logger.info('Removing duplicated video file %s.', logIdentifier)
126 151
127 videoFile.Video.removeFile(videoFile, true) 152 videoFile.Video.removeFile(videoFile, true)
128 .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) 153 .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
154 }
155
156 if (instance.videoStreamingPlaylistId) {
157 const videoStreamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(instance.videoStreamingPlaylistId)
158
159 const videoUUID = videoStreamingPlaylist.Video.uuid
160 logger.info('Removing duplicated video streaming playlist %s.', videoUUID)
161
162 videoStreamingPlaylist.Video.removeStreamingPlaylist(true)
163 .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err }))
164 }
129 165
130 return undefined 166 return undefined
131 } 167 }
@@ -143,7 +179,20 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
143 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) 179 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
144 } 180 }
145 181
146 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 182 static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number) {
183 const actor = await getServerActor()
184
185 const query = {
186 where: {
187 actorId: actor.id,
188 videoStreamingPlaylistId
189 }
190 }
191
192 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
193 }
194
195 static loadByUrl (url: string, transaction?: Transaction) {
147 const query = { 196 const query = {
148 where: { 197 where: {
149 url 198 url
@@ -191,7 +240,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
191 const ids = rows.map(r => r.id) 240 const ids = rows.map(r => r.id)
192 const id = sample(ids) 241 const id = sample(ids)
193 242
194 return VideoModel.loadWithFile(id, undefined, !isTestInstance()) 243 return VideoModel.loadWithFiles(id, undefined, !isTestInstance())
195 } 244 }
196 245
197 static async findMostViewToDuplicate (randomizedFactor: number) { 246 static async findMostViewToDuplicate (randomizedFactor: number) {
@@ -243,7 +292,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
243 where: { 292 where: {
244 privacy: VideoPrivacy.PUBLIC, 293 privacy: VideoPrivacy.PUBLIC,
245 views: { 294 views: {
246 [ Sequelize.Op.gte ]: minViews 295 [ Op.gte ]: minViews
247 } 296 }
248 }, 297 },
249 include: [ 298 include: [
@@ -266,7 +315,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
266 actorId: actor.id, 315 actorId: actor.id,
267 strategy, 316 strategy,
268 createdAt: { 317 createdAt: {
269 [ Sequelize.Op.lt ]: expiredDate 318 [ Op.lt ]: expiredDate
270 } 319 }
271 } 320 }
272 } 321 }
@@ -277,7 +326,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
277 static async getTotalDuplicated (strategy: VideoRedundancyStrategy) { 326 static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
278 const actor = await getServerActor() 327 const actor = await getServerActor()
279 328
280 const options = { 329 const query: FindOptions = {
281 include: [ 330 include: [
282 { 331 {
283 attributes: [], 332 attributes: [],
@@ -291,12 +340,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
291 ] 340 ]
292 } 341 }
293 342
294 return VideoFileModel.sum('size', options as any) // FIXME: typings 343 return VideoFileModel.aggregate('size', 'SUM', query)
295 .then(v => { 344 .then(result => parseAggregateResult(result))
296 if (!v || isNaN(v)) return 0
297
298 return v
299 })
300 } 345 }
301 346
302 static async listLocalExpired () { 347 static async listLocalExpired () {
@@ -306,7 +351,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
306 where: { 351 where: {
307 actorId: actor.id, 352 actorId: actor.id,
308 expiresOn: { 353 expiresOn: {
309 [ Sequelize.Op.lt ]: new Date() 354 [ Op.lt ]: new Date()
310 } 355 }
311 } 356 }
312 } 357 }
@@ -320,10 +365,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
320 const query = { 365 const query = {
321 where: { 366 where: {
322 actorId: { 367 actorId: {
323 [Sequelize.Op.ne]: actor.id 368 [Op.ne]: actor.id
324 }, 369 },
325 expiresOn: { 370 expiresOn: {
326 [ Sequelize.Op.lt ]: new Date() 371 [ Op.lt ]: new Date()
327 } 372 }
328 } 373 }
329 } 374 }
@@ -333,40 +378,44 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
333 378
334 static async listLocalOfServer (serverId: number) { 379 static async listLocalOfServer (serverId: number) {
335 const actor = await getServerActor() 380 const actor = await getServerActor()
336 381 const buildVideoInclude = () => ({
337 const query = { 382 model: VideoModel,
338 where: { 383 required: true,
339 actorId: actor.id
340 },
341 include: [ 384 include: [
342 { 385 {
343 model: VideoFileModel, 386 attributes: [],
387 model: VideoChannelModel.unscoped(),
344 required: true, 388 required: true,
345 include: [ 389 include: [
346 { 390 {
347 model: VideoModel, 391 attributes: [],
392 model: ActorModel.unscoped(),
348 required: true, 393 required: true,
349 include: [ 394 where: {
350 { 395 serverId
351 attributes: [], 396 }
352 model: VideoChannelModel.unscoped(),
353 required: true,
354 include: [
355 {
356 attributes: [],
357 model: ActorModel.unscoped(),
358 required: true,
359 where: {
360 serverId
361 }
362 }
363 ]
364 }
365 ]
366 } 397 }
367 ] 398 ]
368 } 399 }
369 ] 400 ]
401 })
402
403 const query = {
404 where: {
405 actorId: actor.id
406 },
407 include: [
408 {
409 model: VideoFileModel,
410 required: false,
411 include: [ buildVideoInclude() ]
412 },
413 {
414 model: VideoStreamingPlaylistModel,
415 required: false,
416 include: [ buildVideoInclude() ]
417 }
418 ]
370 } 419 }
371 420
372 return VideoRedundancyModel.findAll(query) 421 return VideoRedundancyModel.findAll(query)
@@ -375,12 +424,12 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
375 static async getStats (strategy: VideoRedundancyStrategy) { 424 static async getStats (strategy: VideoRedundancyStrategy) {
376 const actor = await getServerActor() 425 const actor = await getServerActor()
377 426
378 const query = { 427 const query: FindOptions = {
379 raw: true, 428 raw: true,
380 attributes: [ 429 attributes: [
381 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ], 430 [ fn('COALESCE', fn('SUM', col('VideoFile.size')), '0'), 'totalUsed' ],
382 [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ], 431 [ fn('COUNT', fn('DISTINCT', col('videoId'))), 'totalVideos' ],
383 [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ] 432 [ fn('COUNT', col('videoFileId')), 'totalVideoFiles' ]
384 ], 433 ],
385 where: { 434 where: {
386 strategy, 435 strategy,
@@ -395,19 +444,40 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
395 ] 444 ]
396 } 445 }
397 446
398 return VideoRedundancyModel.findOne(query as any) // FIXME: typings 447 return VideoRedundancyModel.findOne(query)
399 .then((r: any) => ({ 448 .then((r: any) => ({
400 totalUsed: parseInt(r.totalUsed.toString(), 10), 449 totalUsed: parseAggregateResult(r.totalUsed),
401 totalVideos: r.totalVideos, 450 totalVideos: r.totalVideos,
402 totalVideoFiles: r.totalVideoFiles 451 totalVideoFiles: r.totalVideoFiles
403 })) 452 }))
404 } 453 }
405 454
455 getVideo () {
456 if (this.VideoFile) return this.VideoFile.Video
457
458 return this.VideoStreamingPlaylist.Video
459 }
460
406 isOwned () { 461 isOwned () {
407 return !!this.strategy 462 return !!this.strategy
408 } 463 }
409 464
410 toActivityPubObject (): CacheFileObject { 465 toActivityPubObject (): CacheFileObject {
466 if (this.VideoStreamingPlaylist) {
467 return {
468 id: this.url,
469 type: 'CacheFile' as 'CacheFile',
470 object: this.VideoStreamingPlaylist.Video.url,
471 expires: this.expiresOn.toISOString(),
472 url: {
473 type: 'Link',
474 mimeType: 'application/x-mpegURL',
475 mediaType: 'application/x-mpegURL',
476 href: this.fileUrl
477 }
478 }
479 }
480
411 return { 481 return {
412 id: this.url, 482 id: this.url,
413 type: 'CacheFile' as 'CacheFile', 483 type: 'CacheFile' as 'CacheFile',
@@ -429,9 +499,9 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
429 private static async buildVideoFileForDuplication () { 499 private static async buildVideoFileForDuplication () {
430 const actor = await getServerActor() 500 const actor = await getServerActor()
431 501
432 const notIn = Sequelize.literal( 502 const notIn = literal(
433 '(' + 503 '(' +
434 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id}` + 504 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` +
435 ')' 505 ')'
436 ) 506 )
437 507
@@ -441,7 +511,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
441 required: true, 511 required: true,
442 where: { 512 where: {
443 id: { 513 id: {
444 [ Sequelize.Op.notIn ]: notIn 514 [ Op.notIn ]: notIn
445 } 515 }
446 } 516 }
447 } 517 }