3c87ec2c13470b70999a98ddf39d723d7c5a37bc
[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 loadByFileId (videoFileId: number) {
129     const query = {
130       where: {
131         videoFileId
132       }
133     }
134
135     return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
136   }
137
138   static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
139     const query = {
140       where: {
141         url
142       },
143       transaction
144     }
145
146     return VideoRedundancyModel.findOne(query)
147   }
148
149   static async getVideoSample (p: Bluebird<VideoModel[]>) {
150     const rows = await p
151     const ids = rows.map(r => r.id)
152     const id = sample(ids)
153
154     return VideoModel.loadWithFile(id, undefined, !isTestInstance())
155   }
156
157   static async findMostViewToDuplicate (randomizedFactor: number) {
158     // On VideoModel!
159     const query = {
160       attributes: [ 'id', 'views' ],
161       limit: randomizedFactor,
162       order: getVideoSort('-views'),
163       where: {
164         privacy: VideoPrivacy.PUBLIC
165       },
166       include: [
167         await VideoRedundancyModel.buildVideoFileForDuplication(),
168         VideoRedundancyModel.buildServerRedundancyInclude()
169       ]
170     }
171
172     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
173   }
174
175   static async findTrendingToDuplicate (randomizedFactor: number) {
176     // On VideoModel!
177     const query = {
178       attributes: [ 'id', 'views' ],
179       subQuery: false,
180       group: 'VideoModel.id',
181       limit: randomizedFactor,
182       order: getVideoSort('-trending'),
183       where: {
184         privacy: VideoPrivacy.PUBLIC
185       },
186       include: [
187         await VideoRedundancyModel.buildVideoFileForDuplication(),
188         VideoRedundancyModel.buildServerRedundancyInclude(),
189
190         VideoModel.buildTrendingQuery(CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS)
191       ]
192     }
193
194     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
195   }
196
197   static async findRecentlyAddedToDuplicate (randomizedFactor: number, minViews: number) {
198     // On VideoModel!
199     const query = {
200       attributes: [ 'id', 'publishedAt' ],
201       limit: randomizedFactor,
202       order: getVideoSort('-publishedAt'),
203       where: {
204         privacy: VideoPrivacy.PUBLIC,
205         views: {
206           [ Sequelize.Op.gte ]: minViews
207         }
208       },
209       include: [
210         await VideoRedundancyModel.buildVideoFileForDuplication(),
211         VideoRedundancyModel.buildServerRedundancyInclude()
212       ]
213     }
214
215     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
216   }
217
218   static async loadOldestLocalThatAlreadyExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number) {
219     const expiredDate = new Date()
220     expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs)
221
222     const actor = await getServerActor()
223
224     const query = {
225       where: {
226         actorId: actor.id,
227         strategy,
228         createdAt: {
229           [ Sequelize.Op.lt ]: expiredDate
230         }
231       }
232     }
233
234     return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findOne(query)
235   }
236
237   static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
238     const actor = await getServerActor()
239
240     const options = {
241       include: [
242         {
243           attributes: [],
244           model: VideoRedundancyModel,
245           required: true,
246           where: {
247             actorId: actor.id,
248             strategy
249           }
250         }
251       ]
252     }
253
254     return VideoFileModel.sum('size', options as any) // FIXME: typings
255   }
256
257   static async listLocalExpired () {
258     const actor = await getServerActor()
259
260     const query = {
261       where: {
262         actorId: actor.id,
263         expiresOn: {
264           [ Sequelize.Op.lt ]: new Date()
265         }
266       }
267     }
268
269     return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
270   }
271
272   static async listRemoteExpired () {
273     const actor = await getServerActor()
274
275     const query = {
276       where: {
277         actorId: {
278           [Sequelize.Op.ne]: actor.id
279         },
280         expiresOn: {
281           [ Sequelize.Op.lt ]: new Date()
282         }
283       }
284     }
285
286     return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
287   }
288
289   static async getStats (strategy: VideoRedundancyStrategy) {
290     const actor = await getServerActor()
291
292     const query = {
293       raw: true,
294       attributes: [
295         [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ],
296         [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ],
297         [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ]
298       ],
299       where: {
300         strategy,
301         actorId: actor.id
302       },
303       include: [
304         {
305           attributes: [],
306           model: VideoFileModel,
307           required: true
308         }
309       ]
310     }
311
312     return VideoRedundancyModel.find(query as any) // FIXME: typings
313       .then((r: any) => ({
314         totalUsed: parseInt(r.totalUsed.toString(), 10),
315         totalVideos: r.totalVideos,
316         totalVideoFiles: r.totalVideoFiles
317       }))
318   }
319
320   toActivityPubObject (): CacheFileObject {
321     return {
322       id: this.url,
323       type: 'CacheFile' as 'CacheFile',
324       object: this.VideoFile.Video.url,
325       expires: this.expiresOn.toISOString(),
326       url: {
327         type: 'Link',
328         mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
329         href: this.fileUrl,
330         height: this.VideoFile.resolution,
331         size: this.VideoFile.size,
332         fps: this.VideoFile.fps
333       }
334     }
335   }
336
337   // Don't include video files we already duplicated
338   private static async buildVideoFileForDuplication () {
339     const actor = await getServerActor()
340
341     const notIn = Sequelize.literal(
342       '(' +
343         `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id}` +
344       ')'
345     )
346
347     return {
348       attributes: [],
349       model: VideoFileModel.unscoped(),
350       required: true,
351       where: {
352         id: {
353           [ Sequelize.Op.notIn ]: notIn
354         }
355       }
356     }
357   }
358
359   private static buildServerRedundancyInclude () {
360     return {
361       attributes: [],
362       model: VideoChannelModel.unscoped(),
363       required: true,
364       include: [
365         {
366           attributes: [],
367           model: ActorModel.unscoped(),
368           required: true,
369           include: [
370             {
371               attributes: [],
372               model: ServerModel.unscoped(),
373               required: true,
374               where: {
375                 redundancyAllowed: true
376               }
377             }
378           ]
379         }
380       ]
381     }
382   }
383 }