c23a9cc17f26417b57c8dd870ee9f6e3d3bfe99c
[oweals/peertube.git] / server / models / redundancy / video-redundancy.ts
1 import {
2   AfterDestroy,
3   AllowNull,
4   BelongsTo,
5   Column,
6   CreatedAt,
7   DataType,
8   ForeignKey,
9   Is,
10   Model,
11   Scopes,
12   Table,
13   UpdatedAt
14 } from 'sequelize-typescript'
15 import { ActorModel } from '../activitypub/actor'
16 import { getVideoSort, throwIfNotValid } from '../utils'
17 import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18 import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers'
19 import { VideoFileModel } from '../video/video-file'
20 import { getServerActor } from '../../helpers/utils'
21 import { VideoModel } from '../video/video'
22 import { VideoRedundancyStrategy } from '../../../shared/models/redundancy'
23 import { logger } from '../../helpers/logger'
24 import { CacheFileObject, VideoPrivacy } from '../../../shared'
25 import { VideoChannelModel } from '../video/video-channel'
26 import { ServerModel } from '../server/server'
27 import { sample } from 'lodash'
28 import { isTestInstance } from '../../helpers/core-utils'
29 import * as Bluebird from 'bluebird'
30 import * as Sequelize from 'sequelize'
31
32 export enum ScopeNames {
33   WITH_VIDEO = 'WITH_VIDEO'
34 }
35
36 @Scopes({
37   [ ScopeNames.WITH_VIDEO ]: {
38     include: [
39       {
40         model: () => VideoFileModel,
41         required: true,
42         include: [
43           {
44             model: () => VideoModel,
45             required: true
46           }
47         ]
48       }
49     ]
50   }
51 })
52
53 @Table({
54   tableName: 'videoRedundancy',
55   indexes: [
56     {
57       fields: [ 'videoFileId' ]
58     },
59     {
60       fields: [ 'actorId' ]
61     },
62     {
63       fields: [ 'url' ],
64       unique: true
65     }
66   ]
67 })
68 export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
69
70   @CreatedAt
71   createdAt: Date
72
73   @UpdatedAt
74   updatedAt: Date
75
76   @AllowNull(false)
77   @Column
78   expiresOn: Date
79
80   @AllowNull(false)
81   @Is('VideoRedundancyFileUrl', value => throwIfNotValid(value, isUrlValid, 'fileUrl'))
82   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
83   fileUrl: string
84
85   @AllowNull(false)
86   @Is('VideoRedundancyUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
87   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
88   url: string
89
90   @AllowNull(true)
91   @Column
92   strategy: string // Only used by us
93
94   @ForeignKey(() => VideoFileModel)
95   @Column
96   videoFileId: number
97
98   @BelongsTo(() => VideoFileModel, {
99     foreignKey: {
100       allowNull: false
101     },
102     onDelete: 'cascade'
103   })
104   VideoFile: VideoFileModel
105
106   @ForeignKey(() => ActorModel)
107   @Column
108   actorId: number
109
110   @BelongsTo(() => ActorModel, {
111     foreignKey: {
112       allowNull: false
113     },
114     onDelete: 'cascade'
115   })
116   Actor: ActorModel
117
118   @AfterDestroy
119   static removeFile (instance: VideoRedundancyModel) {
120     // Not us
121     if (!instance.strategy) return
122
123     logger.info('Removing duplicated video file %s-%s.', instance.VideoFile.Video.uuid, instance.VideoFile.resolution)
124
125     return instance.VideoFile.Video.removeFile(instance.VideoFile)
126   }
127
128   static async loadLocalByFileId (videoFileId: number) {
129     const actor = await getServerActor()
130
131     const query = {
132       where: {
133         actorId: actor.id,
134         videoFileId
135       }
136     }
137
138     return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
139   }
140
141   static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
142     const query = {
143       where: {
144         url
145       },
146       transaction
147     }
148
149     return VideoRedundancyModel.findOne(query)
150   }
151
152   static async isLocalByVideoUUIDExists (uuid: string) {
153     const actor = await getServerActor()
154
155     const query = {
156       raw: true,
157       attributes: [ 'id' ],
158       where: {
159         actorId: actor.id
160       },
161       include: [
162         {
163           attributes: [ ],
164           model: VideoFileModel,
165           required: true,
166           include: [
167             {
168               attributes: [ ],
169               model: VideoModel,
170               required: true,
171               where: {
172                 uuid
173               }
174             }
175           ]
176         }
177       ]
178     }
179
180     return VideoRedundancyModel.findOne(query)
181       .then(r => !!r)
182   }
183
184   static async getVideoSample (p: Bluebird<VideoModel[]>) {
185     const rows = await p
186     const ids = rows.map(r => r.id)
187     const id = sample(ids)
188
189     return VideoModel.loadWithFile(id, undefined, !isTestInstance())
190   }
191
192   static async findMostViewToDuplicate (randomizedFactor: number) {
193     // On VideoModel!
194     const query = {
195       attributes: [ 'id', 'views' ],
196       limit: randomizedFactor,
197       order: getVideoSort('-views'),
198       where: {
199         privacy: VideoPrivacy.PUBLIC
200       },
201       include: [
202         await VideoRedundancyModel.buildVideoFileForDuplication(),
203         VideoRedundancyModel.buildServerRedundancyInclude()
204       ]
205     }
206
207     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
208   }
209
210   static async findTrendingToDuplicate (randomizedFactor: number) {
211     // On VideoModel!
212     const query = {
213       attributes: [ 'id', 'views' ],
214       subQuery: false,
215       group: 'VideoModel.id',
216       limit: randomizedFactor,
217       order: getVideoSort('-trending'),
218       where: {
219         privacy: VideoPrivacy.PUBLIC
220       },
221       include: [
222         await VideoRedundancyModel.buildVideoFileForDuplication(),
223         VideoRedundancyModel.buildServerRedundancyInclude(),
224
225         VideoModel.buildTrendingQuery(CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS)
226       ]
227     }
228
229     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
230   }
231
232   static async findRecentlyAddedToDuplicate (randomizedFactor: number, minViews: number) {
233     // On VideoModel!
234     const query = {
235       attributes: [ 'id', 'publishedAt' ],
236       limit: randomizedFactor,
237       order: getVideoSort('-publishedAt'),
238       where: {
239         privacy: VideoPrivacy.PUBLIC,
240         views: {
241           [ Sequelize.Op.gte ]: minViews
242         }
243       },
244       include: [
245         await VideoRedundancyModel.buildVideoFileForDuplication(),
246         VideoRedundancyModel.buildServerRedundancyInclude()
247       ]
248     }
249
250     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
251   }
252
253   static async loadOldestLocalThatAlreadyExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number) {
254     const expiredDate = new Date()
255     expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs)
256
257     const actor = await getServerActor()
258
259     const query = {
260       where: {
261         actorId: actor.id,
262         strategy,
263         createdAt: {
264           [ Sequelize.Op.lt ]: expiredDate
265         }
266       }
267     }
268
269     return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findOne(query)
270   }
271
272   static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
273     const actor = await getServerActor()
274
275     const options = {
276       include: [
277         {
278           attributes: [],
279           model: VideoRedundancyModel,
280           required: true,
281           where: {
282             actorId: actor.id,
283             strategy
284           }
285         }
286       ]
287     }
288
289     return VideoFileModel.sum('size', options as any) // FIXME: typings
290   }
291
292   static async listLocalExpired () {
293     const actor = await getServerActor()
294
295     const query = {
296       where: {
297         actorId: actor.id,
298         expiresOn: {
299           [ Sequelize.Op.lt ]: new Date()
300         }
301       }
302     }
303
304     return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
305   }
306
307   static async listRemoteExpired () {
308     const actor = await getServerActor()
309
310     const query = {
311       where: {
312         actorId: {
313           [Sequelize.Op.ne]: actor.id
314         },
315         expiresOn: {
316           [ Sequelize.Op.lt ]: new Date()
317         }
318       }
319     }
320
321     return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
322   }
323
324   static async listLocalOfServer (serverId: number) {
325     const actor = await getServerActor()
326
327     const query = {
328       where: {
329         actorId: actor.id
330       },
331       include: [
332         {
333           model: VideoFileModel,
334           required: true,
335           include: [
336             {
337               model: VideoModel,
338               required: true,
339               include: [
340                 {
341                   attributes: [],
342                   model: VideoChannelModel.unscoped(),
343                   required: true,
344                   include: [
345                     {
346                       attributes: [],
347                       model: ActorModel.unscoped(),
348                       required: true,
349                       where: {
350                         serverId
351                       }
352                     }
353                   ]
354                 }
355               ]
356             }
357           ]
358         }
359       ]
360     }
361
362     return VideoRedundancyModel.findAll(query)
363   }
364
365   static async getStats (strategy: VideoRedundancyStrategy) {
366     const actor = await getServerActor()
367
368     const query = {
369       raw: true,
370       attributes: [
371         [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ],
372         [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ],
373         [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ]
374       ],
375       where: {
376         strategy,
377         actorId: actor.id
378       },
379       include: [
380         {
381           attributes: [],
382           model: VideoFileModel,
383           required: true
384         }
385       ]
386     }
387
388     return VideoRedundancyModel.find(query as any) // FIXME: typings
389       .then((r: any) => ({
390         totalUsed: parseInt(r.totalUsed.toString(), 10),
391         totalVideos: r.totalVideos,
392         totalVideoFiles: r.totalVideoFiles
393       }))
394   }
395
396   toActivityPubObject (): CacheFileObject {
397     return {
398       id: this.url,
399       type: 'CacheFile' as 'CacheFile',
400       object: this.VideoFile.Video.url,
401       expires: this.expiresOn.toISOString(),
402       url: {
403         type: 'Link',
404         mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
405         href: this.fileUrl,
406         height: this.VideoFile.resolution,
407         size: this.VideoFile.size,
408         fps: this.VideoFile.fps
409       }
410     }
411   }
412
413   // Don't include video files we already duplicated
414   private static async buildVideoFileForDuplication () {
415     const actor = await getServerActor()
416
417     const notIn = Sequelize.literal(
418       '(' +
419         `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id}` +
420       ')'
421     )
422
423     return {
424       attributes: [],
425       model: VideoFileModel.unscoped(),
426       required: true,
427       where: {
428         id: {
429           [ Sequelize.Op.notIn ]: notIn
430         }
431       }
432     }
433   }
434
435   private static buildServerRedundancyInclude () {
436     return {
437       attributes: [],
438       model: VideoChannelModel.unscoped(),
439       required: true,
440       include: [
441         {
442           attributes: [],
443           model: ActorModel.unscoped(),
444           required: true,
445           include: [
446             {
447               attributes: [],
448               model: ServerModel.unscoped(),
449               required: true,
450               where: {
451                 redundancyAllowed: true
452               }
453             }
454           ]
455         }
456       ]
457     }
458   }
459 }