diff options
Diffstat (limited to 'server')
85 files changed, 895 insertions, 327 deletions
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts index 5f49336b1..d1d4ef765 100644 --- a/server/controllers/api/index.ts +++ b/server/controllers/api/index.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import RateLimit from 'express-rate-limit' | 3 | import { buildRateLimiter } from '@server/middlewares' |
4 | import { HttpStatusCode } from '../../../shared/models' | 4 | import { HttpStatusCode } from '../../../shared/models' |
5 | import { badRequest } from '../../helpers/express-utils' | 5 | import { badRequest } from '../../helpers/express-utils' |
6 | import { CONFIG } from '../../initializers/config' | 6 | import { CONFIG } from '../../initializers/config' |
@@ -29,7 +29,7 @@ apiRouter.use(cors({ | |||
29 | credentials: true | 29 | credentials: true |
30 | })) | 30 | })) |
31 | 31 | ||
32 | const apiRateLimiter = RateLimit({ | 32 | const apiRateLimiter = buildRateLimiter({ |
33 | windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS, | 33 | windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS, |
34 | max: CONFIG.RATES_LIMIT.API.MAX | 34 | max: CONFIG.RATES_LIMIT.API.MAX |
35 | }) | 35 | }) |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 8a06bfe93..46e80d56d 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import RateLimit from 'express-rate-limit' | ||
3 | import { tokensRouter } from '@server/controllers/api/users/token' | 2 | import { tokensRouter } from '@server/controllers/api/users/token' |
4 | import { Hooks } from '@server/lib/plugins/hooks' | 3 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 4 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
@@ -17,9 +16,11 @@ import { Notifier } from '../../../lib/notifier' | |||
17 | import { Redis } from '../../../lib/redis' | 16 | import { Redis } from '../../../lib/redis' |
18 | import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' | 17 | import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' |
19 | import { | 18 | import { |
19 | adminUsersSortValidator, | ||
20 | asyncMiddleware, | 20 | asyncMiddleware, |
21 | asyncRetryTransactionMiddleware, | 21 | asyncRetryTransactionMiddleware, |
22 | authenticate, | 22 | authenticate, |
23 | buildRateLimiter, | ||
23 | ensureUserHasRight, | 24 | ensureUserHasRight, |
24 | ensureUserRegistrationAllowed, | 25 | ensureUserRegistrationAllowed, |
25 | ensureUserRegistrationAllowedForIP, | 26 | ensureUserRegistrationAllowedForIP, |
@@ -32,7 +33,6 @@ import { | |||
32 | usersListValidator, | 33 | usersListValidator, |
33 | usersRegisterValidator, | 34 | usersRegisterValidator, |
34 | usersRemoveValidator, | 35 | usersRemoveValidator, |
35 | usersSortValidator, | ||
36 | usersUpdateValidator | 36 | usersUpdateValidator |
37 | } from '../../../middlewares' | 37 | } from '../../../middlewares' |
38 | import { | 38 | import { |
@@ -54,13 +54,13 @@ import { myVideoPlaylistsRouter } from './my-video-playlists' | |||
54 | 54 | ||
55 | const auditLogger = auditLoggerFactory('users') | 55 | const auditLogger = auditLoggerFactory('users') |
56 | 56 | ||
57 | const signupRateLimiter = RateLimit({ | 57 | const signupRateLimiter = buildRateLimiter({ |
58 | windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS, | 58 | windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS, |
59 | max: CONFIG.RATES_LIMIT.SIGNUP.MAX, | 59 | max: CONFIG.RATES_LIMIT.SIGNUP.MAX, |
60 | skipFailedRequests: true | 60 | skipFailedRequests: true |
61 | }) | 61 | }) |
62 | 62 | ||
63 | const askSendEmailLimiter = RateLimit({ | 63 | const askSendEmailLimiter = buildRateLimiter({ |
64 | windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, | 64 | windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, |
65 | max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX | 65 | max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX |
66 | }) | 66 | }) |
@@ -84,7 +84,7 @@ usersRouter.get('/', | |||
84 | authenticate, | 84 | authenticate, |
85 | ensureUserHasRight(UserRight.MANAGE_USERS), | 85 | ensureUserHasRight(UserRight.MANAGE_USERS), |
86 | paginationValidator, | 86 | paginationValidator, |
87 | usersSortValidator, | 87 | adminUsersSortValidator, |
88 | setDefaultSort, | 88 | setDefaultSort, |
89 | setDefaultPagination, | 89 | setDefaultPagination, |
90 | usersListValidator, | 90 | usersListValidator, |
@@ -277,7 +277,7 @@ async function autocompleteUsers (req: express.Request, res: express.Response) { | |||
277 | } | 277 | } |
278 | 278 | ||
279 | async function listUsers (req: express.Request, res: express.Response) { | 279 | async function listUsers (req: express.Request, res: express.Response) { |
280 | const resultList = await UserModel.listForApi({ | 280 | const resultList = await UserModel.listForAdminApi({ |
281 | start: req.query.start, | 281 | start: req.query.start, |
282 | count: req.query.count, | 282 | count: req.query.count, |
283 | sort: req.query.sort, | 283 | sort: req.query.sort, |
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts index 258b50fe9..012a49791 100644 --- a/server/controllers/api/users/token.ts +++ b/server/controllers/api/users/token.ts | |||
@@ -1,18 +1,17 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import RateLimit from 'express-rate-limit' | ||
3 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
4 | import { CONFIG } from '@server/initializers/config' | 3 | import { CONFIG } from '@server/initializers/config' |
5 | import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' | 4 | import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' |
6 | import { handleOAuthToken } from '@server/lib/auth/oauth' | 5 | import { handleOAuthToken } from '@server/lib/auth/oauth' |
7 | import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' | 6 | import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' |
8 | import { Hooks } from '@server/lib/plugins/hooks' | 7 | import { Hooks } from '@server/lib/plugins/hooks' |
9 | import { asyncMiddleware, authenticate, openapiOperationDoc } from '@server/middlewares' | 8 | import { asyncMiddleware, authenticate, buildRateLimiter, openapiOperationDoc } from '@server/middlewares' |
10 | import { buildUUID } from '@shared/extra-utils' | 9 | import { buildUUID } from '@shared/extra-utils' |
11 | import { ScopedToken } from '@shared/models/users/user-scoped-token' | 10 | import { ScopedToken } from '@shared/models/users/user-scoped-token' |
12 | 11 | ||
13 | const tokensRouter = express.Router() | 12 | const tokensRouter = express.Router() |
14 | 13 | ||
15 | const loginRateLimiter = RateLimit({ | 14 | const loginRateLimiter = buildRateLimiter({ |
16 | windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS, | 15 | windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS, |
17 | max: CONFIG.RATES_LIMIT.LOGIN.MAX | 16 | max: CONFIG.RATES_LIMIT.LOGIN.MAX |
18 | }) | 17 | }) |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index be233722c..d4e08293e 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -26,6 +26,7 @@ import { | |||
26 | setDefaultVideosSort, | 26 | setDefaultVideosSort, |
27 | videosCustomGetValidator, | 27 | videosCustomGetValidator, |
28 | videosGetValidator, | 28 | videosGetValidator, |
29 | videoSourceGetValidator, | ||
29 | videosRemoveValidator, | 30 | videosRemoveValidator, |
30 | videosSortValidator | 31 | videosSortValidator |
31 | } from '../../../middlewares' | 32 | } from '../../../middlewares' |
@@ -96,6 +97,14 @@ videosRouter.get('/:id/description', | |||
96 | asyncMiddleware(videosGetValidator), | 97 | asyncMiddleware(videosGetValidator), |
97 | asyncMiddleware(getVideoDescription) | 98 | asyncMiddleware(getVideoDescription) |
98 | ) | 99 | ) |
100 | |||
101 | videosRouter.get('/:id/source', | ||
102 | openapiOperationDoc({ operationId: 'getVideoSource' }), | ||
103 | authenticate, | ||
104 | asyncMiddleware(videoSourceGetValidator), | ||
105 | getVideoSource | ||
106 | ) | ||
107 | |||
99 | videosRouter.get('/:id', | 108 | videosRouter.get('/:id', |
100 | openapiOperationDoc({ operationId: 'getVideo' }), | 109 | openapiOperationDoc({ operationId: 'getVideo' }), |
101 | optionalAuthenticate, | 110 | optionalAuthenticate, |
@@ -155,6 +164,10 @@ async function getVideoDescription (req: express.Request, res: express.Response) | |||
155 | return res.json({ description }) | 164 | return res.json({ description }) |
156 | } | 165 | } |
157 | 166 | ||
167 | function getVideoSource (req: express.Request, res: express.Response) { | ||
168 | return res.json(res.locals.videoSource.toFormattedJSON()) | ||
169 | } | ||
170 | |||
158 | async function listVideos (req: express.Request, res: express.Response) { | 171 | async function listVideos (req: express.Request, res: express.Response) { |
159 | const serverActor = await getServerActor() | 172 | const serverActor = await getServerActor() |
160 | 173 | ||
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts index 3afbedbb2..c5890691e 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts | |||
@@ -44,6 +44,7 @@ import { | |||
44 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 44 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
45 | import { VideoModel } from '../../../models/video/video' | 45 | import { VideoModel } from '../../../models/video/video' |
46 | import { VideoFileModel } from '../../../models/video/video-file' | 46 | import { VideoFileModel } from '../../../models/video/video-file' |
47 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
47 | 48 | ||
48 | const lTags = loggerTagsFactory('api', 'video') | 49 | const lTags = loggerTagsFactory('api', 'video') |
49 | const auditLogger = auditLoggerFactory('videos') | 50 | const auditLogger = auditLoggerFactory('videos') |
@@ -151,6 +152,7 @@ async function addVideo (options: { | |||
151 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | 152 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object |
152 | 153 | ||
153 | const videoFile = await buildNewFile(videoPhysicalFile) | 154 | const videoFile = await buildNewFile(videoPhysicalFile) |
155 | const originalFilename = videoPhysicalFile.originalname | ||
154 | 156 | ||
155 | // Move physical file | 157 | // Move physical file |
156 | const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile) | 158 | const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile) |
@@ -181,6 +183,11 @@ async function addVideo (options: { | |||
181 | 183 | ||
182 | video.VideoFiles = [ videoFile ] | 184 | video.VideoFiles = [ videoFile ] |
183 | 185 | ||
186 | await VideoSourceModel.create({ | ||
187 | filename: originalFilename, | ||
188 | videoId: video.id | ||
189 | }, { transaction: t }) | ||
190 | |||
184 | await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) | 191 | await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) |
185 | 192 | ||
186 | // Schedule an update in the future? | 193 | // Schedule an update in the future? |
diff --git a/server/controllers/api/videos/view.ts b/server/controllers/api/videos/view.ts index db1091f2d..dee1ec67c 100644 --- a/server/controllers/api/videos/view.ts +++ b/server/controllers/api/videos/view.ts | |||
@@ -26,7 +26,7 @@ export { | |||
26 | // --------------------------------------------------------------------------- | 26 | // --------------------------------------------------------------------------- |
27 | 27 | ||
28 | async function viewVideo (req: express.Request, res: express.Response) { | 28 | async function viewVideo (req: express.Request, res: express.Response) { |
29 | const video = res.locals.onlyVideo | 29 | const video = res.locals.onlyImmutableVideo |
30 | 30 | ||
31 | const body = req.body as VideoView | 31 | const body = req.body as VideoView |
32 | 32 | ||
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index ad93d700f..241715fb9 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { Feed } from '@peertube/feed' | ||
3 | import { extname } from 'path' | 2 | import { extname } from 'path' |
3 | import { Feed } from '@peertube/feed' | ||
4 | import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown' | 4 | import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' | 6 | import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' |
7 | import { VideoInclude } from '@shared/models' | 7 | import { VideoInclude } from '@shared/models' |
8 | import { buildNSFWFilter } from '../helpers/express-utils' | 8 | import { buildNSFWFilter } from '../helpers/express-utils' |
9 | import { CONFIG } from '../initializers/config' | 9 | import { CONFIG } from '../initializers/config' |
10 | import { FEEDS, MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' | 10 | import { MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' |
11 | import { | 11 | import { |
12 | asyncMiddleware, | 12 | asyncMiddleware, |
13 | commonVideosFiltersValidator, | 13 | commonVideosFiltersValidator, |
@@ -76,7 +76,7 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res | |||
76 | 76 | ||
77 | const comments = await VideoCommentModel.listForFeed({ | 77 | const comments = await VideoCommentModel.listForFeed({ |
78 | start, | 78 | start, |
79 | count: FEEDS.COUNT, | 79 | count: CONFIG.FEEDS.COMMENTS.COUNT, |
80 | videoId: video ? video.id : undefined, | 80 | videoId: video ? video.id : undefined, |
81 | accountId: account ? account.id : undefined, | 81 | accountId: account ? account.id : undefined, |
82 | videoChannelId: videoChannel ? videoChannel.id : undefined | 82 | videoChannelId: videoChannel ? videoChannel.id : undefined |
@@ -166,7 +166,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response) { | |||
166 | const server = await getServerActor() | 166 | const server = await getServerActor() |
167 | const { data } = await VideoModel.listForApi({ | 167 | const { data } = await VideoModel.listForApi({ |
168 | start, | 168 | start, |
169 | count: FEEDS.COUNT, | 169 | count: CONFIG.FEEDS.VIDEOS.COUNT, |
170 | sort: req.query.sort, | 170 | sort: req.query.sort, |
171 | displayOnlyForFollower: { | 171 | displayOnlyForFollower: { |
172 | actorId: server.id, | 172 | actorId: server.id, |
@@ -202,7 +202,7 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp | |||
202 | 202 | ||
203 | const { data } = await VideoModel.listForApi({ | 203 | const { data } = await VideoModel.listForApi({ |
204 | start, | 204 | start, |
205 | count: FEEDS.COUNT, | 205 | count: CONFIG.FEEDS.VIDEOS.COUNT, |
206 | sort: req.query.sort, | 206 | sort: req.query.sort, |
207 | nsfw, | 207 | nsfw, |
208 | 208 | ||
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts index 55bf02660..0cab5dcd0 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' | 3 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' |
4 | import { MActorImage } from '@server/types/models' | ||
4 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | 5 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' |
5 | import { logger } from '../helpers/logger' | 6 | import { logger } from '../helpers/logger' |
6 | import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' | 7 | import { ACTOR_IMAGES_SIZE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' |
7 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' | 8 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' |
8 | import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/local-actor' | 9 | import { actorImagePathUnsafeCache, downloadActorImageFromWorker } from '../lib/local-actor' |
9 | import { asyncMiddleware } from '../middlewares' | 10 | import { asyncMiddleware } from '../middlewares' |
10 | import { ActorImageModel } from '../models/actor/actor-image' | 11 | import { ActorImageModel } from '../models/actor/actor-image' |
11 | 12 | ||
@@ -64,13 +65,10 @@ async function getActorImage (req: express.Request, res: express.Response, next: | |||
64 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) | 65 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) |
65 | 66 | ||
66 | try { | 67 | try { |
67 | await pushActorImageProcessInQueue({ | 68 | await downloadActorImageFromWorker({ |
68 | filename: image.filename, | 69 | filename: image.filename, |
69 | fileUrl: image.fileUrl, | 70 | fileUrl: image.fileUrl, |
70 | size: { | 71 | size: getActorImageSize(image), |
71 | height: image.height, | ||
72 | width: image.width | ||
73 | }, | ||
74 | type: image.type | 72 | type: image.type |
75 | }) | 73 | }) |
76 | } catch (err) { | 74 | } catch (err) { |
@@ -94,7 +92,7 @@ async function getActorImage (req: express.Request, res: express.Response, next: | |||
94 | if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) { | 92 | if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) { |
95 | logger.error('Cannot lazy serve actor image %s.', filename, { err }) | 93 | logger.error('Cannot lazy serve actor image %s.', filename, { err }) |
96 | 94 | ||
97 | actorImagePathUnsafeCache.del(filename) | 95 | actorImagePathUnsafeCache.delete(filename) |
98 | 96 | ||
99 | image.onDisk = false | 97 | image.onDisk = false |
100 | image.save() | 98 | image.save() |
@@ -105,6 +103,17 @@ async function getActorImage (req: express.Request, res: express.Response, next: | |||
105 | }) | 103 | }) |
106 | } | 104 | } |
107 | 105 | ||
106 | function getActorImageSize (image: MActorImage): { width: number, height: number } { | ||
107 | if (image.width && image.height) { | ||
108 | return { | ||
109 | height: image.height, | ||
110 | width: image.width | ||
111 | } | ||
112 | } | ||
113 | |||
114 | return ACTOR_IMAGES_SIZE[image.type][0] | ||
115 | } | ||
116 | |||
108 | async function getPreview (req: express.Request, res: express.Response) { | 117 | async function getPreview (req: express.Request, res: express.Response) { |
109 | const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) | 118 | const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) |
110 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 119 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
diff --git a/server/controllers/services.ts b/server/controllers/services.ts index 9151e1b04..70d08ab69 100644 --- a/server/controllers/services.ts +++ b/server/controllers/services.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { EMBED_SIZE, PREVIEWS_SIZE, WEBSERVER, THUMBNAILS_SIZE } from '../initializers/constants' | ||
3 | import { asyncMiddleware, oembedValidator } from '../middlewares' | ||
4 | import { accountNameWithHostGetValidator } from '../middlewares/validators' | ||
5 | import { MChannelSummary } from '@server/types/models' | 2 | import { MChannelSummary } from '@server/types/models' |
6 | import { escapeHTML } from '@shared/core-utils/renderer' | 3 | import { escapeHTML } from '@shared/core-utils/renderer' |
4 | import { EMBED_SIZE, PREVIEWS_SIZE, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants' | ||
5 | import { asyncMiddleware, oembedValidator } from '../middlewares' | ||
6 | import { accountNameWithHostGetValidator } from '../middlewares/validators' | ||
7 | 7 | ||
8 | const servicesRouter = express.Router() | 8 | const servicesRouter = express.Router() |
9 | 9 | ||
@@ -36,7 +36,7 @@ function generatePlaylistOEmbed (req: express.Request, res: express.Response) { | |||
36 | const json = buildOEmbed({ | 36 | const json = buildOEmbed({ |
37 | channel: playlist.VideoChannel, | 37 | channel: playlist.VideoChannel, |
38 | title: playlist.name, | 38 | title: playlist.name, |
39 | embedPath: playlist.getEmbedStaticPath(), | 39 | embedPath: playlist.getEmbedStaticPath() + buildPlayerURLQuery(req.query.url), |
40 | previewPath: playlist.getThumbnailStaticPath(), | 40 | previewPath: playlist.getThumbnailStaticPath(), |
41 | previewSize: THUMBNAILS_SIZE, | 41 | previewSize: THUMBNAILS_SIZE, |
42 | req | 42 | req |
@@ -51,7 +51,7 @@ function generateVideoOEmbed (req: express.Request, res: express.Response) { | |||
51 | const json = buildOEmbed({ | 51 | const json = buildOEmbed({ |
52 | channel: video.VideoChannel, | 52 | channel: video.VideoChannel, |
53 | title: video.name, | 53 | title: video.name, |
54 | embedPath: video.getEmbedStaticPath(), | 54 | embedPath: video.getEmbedStaticPath() + buildPlayerURLQuery(req.query.url), |
55 | previewPath: video.getPreviewStaticPath(), | 55 | previewPath: video.getPreviewStaticPath(), |
56 | previewSize: PREVIEWS_SIZE, | 56 | previewSize: PREVIEWS_SIZE, |
57 | req | 57 | req |
@@ -60,6 +60,40 @@ function generateVideoOEmbed (req: express.Request, res: express.Response) { | |||
60 | return res.json(json) | 60 | return res.json(json) |
61 | } | 61 | } |
62 | 62 | ||
63 | function buildPlayerURLQuery (inputQueryUrl: string) { | ||
64 | const allowedParameters = new Set([ | ||
65 | 'start', | ||
66 | 'stop', | ||
67 | 'loop', | ||
68 | 'autoplay', | ||
69 | 'muted', | ||
70 | 'controls', | ||
71 | 'controlBar', | ||
72 | 'title', | ||
73 | 'api', | ||
74 | 'warningTitle', | ||
75 | 'peertubeLink', | ||
76 | 'p2p', | ||
77 | 'subtitle', | ||
78 | 'bigPlayBackgroundColor', | ||
79 | 'mode', | ||
80 | 'foregroundColor' | ||
81 | ]) | ||
82 | |||
83 | const params = new URLSearchParams() | ||
84 | |||
85 | new URL(inputQueryUrl).searchParams.forEach((v, k) => { | ||
86 | if (allowedParameters.has(k)) { | ||
87 | params.append(k, v) | ||
88 | } | ||
89 | }) | ||
90 | |||
91 | const stringQuery = params.toString() | ||
92 | if (!stringQuery) return '' | ||
93 | |||
94 | return '?' + stringQuery | ||
95 | } | ||
96 | |||
63 | function buildOEmbed (options: { | 97 | function buildOEmbed (options: { |
64 | req: express.Request | 98 | req: express.Request |
65 | title: string | 99 | title: string |
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts index 79ef44be1..076b7f11d 100644 --- a/server/helpers/audit-logger.ts +++ b/server/helpers/audit-logger.ts | |||
@@ -120,7 +120,7 @@ const videoKeysToKeep = [ | |||
120 | 'downloadEnabled' | 120 | 'downloadEnabled' |
121 | ] | 121 | ] |
122 | class VideoAuditView extends EntityAuditView { | 122 | class VideoAuditView extends EntityAuditView { |
123 | constructor (private readonly video: VideoDetails) { | 123 | constructor (video: VideoDetails) { |
124 | super(videoKeysToKeep, 'video', video) | 124 | super(videoKeysToKeep, 'video', video) |
125 | } | 125 | } |
126 | } | 126 | } |
@@ -131,7 +131,7 @@ const videoImportKeysToKeep = [ | |||
131 | 'video-name' | 131 | 'video-name' |
132 | ] | 132 | ] |
133 | class VideoImportAuditView extends EntityAuditView { | 133 | class VideoImportAuditView extends EntityAuditView { |
134 | constructor (private readonly videoImport: VideoImport) { | 134 | constructor (videoImport: VideoImport) { |
135 | super(videoImportKeysToKeep, 'video-import', videoImport) | 135 | super(videoImportKeysToKeep, 'video-import', videoImport) |
136 | } | 136 | } |
137 | } | 137 | } |
@@ -150,7 +150,7 @@ const commentKeysToKeep = [ | |||
150 | 'account-name' | 150 | 'account-name' |
151 | ] | 151 | ] |
152 | class CommentAuditView extends EntityAuditView { | 152 | class CommentAuditView extends EntityAuditView { |
153 | constructor (private readonly comment: VideoComment) { | 153 | constructor (comment: VideoComment) { |
154 | super(commentKeysToKeep, 'comment', comment) | 154 | super(commentKeysToKeep, 'comment', comment) |
155 | } | 155 | } |
156 | } | 156 | } |
@@ -179,7 +179,7 @@ const userKeysToKeep = [ | |||
179 | 'videoChannels' | 179 | 'videoChannels' |
180 | ] | 180 | ] |
181 | class UserAuditView extends EntityAuditView { | 181 | class UserAuditView extends EntityAuditView { |
182 | constructor (private readonly user: User) { | 182 | constructor (user: User) { |
183 | super(userKeysToKeep, 'user', user) | 183 | super(userKeysToKeep, 'user', user) |
184 | } | 184 | } |
185 | } | 185 | } |
@@ -205,7 +205,7 @@ const channelKeysToKeep = [ | |||
205 | 'ownerAccount-displayedName' | 205 | 'ownerAccount-displayedName' |
206 | ] | 206 | ] |
207 | class VideoChannelAuditView extends EntityAuditView { | 207 | class VideoChannelAuditView extends EntityAuditView { |
208 | constructor (private readonly channel: VideoChannel) { | 208 | constructor (channel: VideoChannel) { |
209 | super(channelKeysToKeep, 'channel', channel) | 209 | super(channelKeysToKeep, 'channel', channel) |
210 | } | 210 | } |
211 | } | 211 | } |
@@ -217,7 +217,7 @@ const abuseKeysToKeep = [ | |||
217 | 'createdAt' | 217 | 'createdAt' |
218 | ] | 218 | ] |
219 | class AbuseAuditView extends EntityAuditView { | 219 | class AbuseAuditView extends EntityAuditView { |
220 | constructor (private readonly abuse: AdminAbuse) { | 220 | constructor (abuse: AdminAbuse) { |
221 | super(abuseKeysToKeep, 'abuse', abuse) | 221 | super(abuseKeysToKeep, 'abuse', abuse) |
222 | } | 222 | } |
223 | } | 223 | } |
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 0ec45eb2e..6ebe8e2ac 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -56,6 +56,7 @@ const timeTable = { | |||
56 | export function parseDurationToMs (duration: number | string): number { | 56 | export function parseDurationToMs (duration: number | string): number { |
57 | if (duration === null) return null | 57 | if (duration === null) return null |
58 | if (typeof duration === 'number') return duration | 58 | if (typeof duration === 'number') return duration |
59 | if (!isNaN(+duration)) return +duration | ||
59 | 60 | ||
60 | if (typeof duration === 'string') { | 61 | if (typeof duration === 'string') { |
61 | const split = duration.match(/^([\d.,]+)\s?(\w+)$/) | 62 | const split = duration.match(/^([\d.,]+)\s?(\w+)$/) |
@@ -76,6 +77,7 @@ export function parseDurationToMs (duration: number | string): number { | |||
76 | 77 | ||
77 | export function parseBytes (value: string | number): number { | 78 | export function parseBytes (value: string | number): number { |
78 | if (typeof value === 'number') return value | 79 | if (typeof value === 'number') return value |
80 | if (!isNaN(+value)) return +value | ||
79 | 81 | ||
80 | const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/ | 82 | const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/ |
81 | const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/ | 83 | const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/ |
@@ -85,40 +87,55 @@ export function parseBytes (value: string | number): number { | |||
85 | const g = /^(\d+)\s*GB$/ | 87 | const g = /^(\d+)\s*GB$/ |
86 | const m = /^(\d+)\s*MB$/ | 88 | const m = /^(\d+)\s*MB$/ |
87 | const b = /^(\d+)\s*B$/ | 89 | const b = /^(\d+)\s*B$/ |
88 | let match | 90 | |
91 | let match: RegExpMatchArray | ||
89 | 92 | ||
90 | if (value.match(tgm)) { | 93 | if (value.match(tgm)) { |
91 | match = value.match(tgm) | 94 | match = value.match(tgm) |
92 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + | 95 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + |
93 | parseInt(match[2], 10) * 1024 * 1024 * 1024 + | 96 | parseInt(match[2], 10) * 1024 * 1024 * 1024 + |
94 | parseInt(match[3], 10) * 1024 * 1024 | 97 | parseInt(match[3], 10) * 1024 * 1024 |
95 | } else if (value.match(tg)) { | 98 | } |
99 | |||
100 | if (value.match(tg)) { | ||
96 | match = value.match(tg) | 101 | match = value.match(tg) |
97 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + | 102 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + |
98 | parseInt(match[2], 10) * 1024 * 1024 * 1024 | 103 | parseInt(match[2], 10) * 1024 * 1024 * 1024 |
99 | } else if (value.match(tm)) { | 104 | } |
105 | |||
106 | if (value.match(tm)) { | ||
100 | match = value.match(tm) | 107 | match = value.match(tm) |
101 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + | 108 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + |
102 | parseInt(match[2], 10) * 1024 * 1024 | 109 | parseInt(match[2], 10) * 1024 * 1024 |
103 | } else if (value.match(gm)) { | 110 | } |
111 | |||
112 | if (value.match(gm)) { | ||
104 | match = value.match(gm) | 113 | match = value.match(gm) |
105 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 + | 114 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 + |
106 | parseInt(match[2], 10) * 1024 * 1024 | 115 | parseInt(match[2], 10) * 1024 * 1024 |
107 | } else if (value.match(t)) { | 116 | } |
117 | |||
118 | if (value.match(t)) { | ||
108 | match = value.match(t) | 119 | match = value.match(t) |
109 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | 120 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 |
110 | } else if (value.match(g)) { | 121 | } |
122 | |||
123 | if (value.match(g)) { | ||
111 | match = value.match(g) | 124 | match = value.match(g) |
112 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 | 125 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 |
113 | } else if (value.match(m)) { | 126 | } |
127 | |||
128 | if (value.match(m)) { | ||
114 | match = value.match(m) | 129 | match = value.match(m) |
115 | return parseInt(match[1], 10) * 1024 * 1024 | 130 | return parseInt(match[1], 10) * 1024 * 1024 |
116 | } else if (value.match(b)) { | 131 | } |
132 | |||
133 | if (value.match(b)) { | ||
117 | match = value.match(b) | 134 | match = value.match(b) |
118 | return parseInt(match[1], 10) * 1024 | 135 | return parseInt(match[1], 10) * 1024 |
119 | } else { | ||
120 | return parseInt(value, 10) | ||
121 | } | 136 | } |
137 | |||
138 | return parseInt(value, 10) | ||
122 | } | 139 | } |
123 | 140 | ||
124 | // --------------------------------------------------------------------------- | 141 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index 7d6451db9..ebb102a0d 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts | |||
@@ -114,7 +114,7 @@ async function autoResize (options: { | |||
114 | }) { | 114 | }) { |
115 | const { sourceImage, newSize, destination } = options | 115 | const { sourceImage, newSize, destination } = options |
116 | 116 | ||
117 | // Portrait mode targetting a landscape, apply some effect on the image | 117 | // Portrait mode targeting a landscape, apply some effect on the image |
118 | const sourceIsPortrait = sourceImage.getWidth() < sourceImage.getHeight() | 118 | const sourceIsPortrait = sourceImage.getWidth() < sourceImage.getHeight() |
119 | const destIsPortraitOrSquare = newSize.width <= newSize.height | 119 | const destIsPortraitOrSquare = newSize.width <= newSize.height |
120 | 120 | ||
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts index 5fe3646c5..4fbaf8a73 100644 --- a/server/helpers/logger.ts +++ b/server/helpers/logger.ts | |||
@@ -57,7 +57,7 @@ const consoleLoggerFormat = format.printf(info => { | |||
57 | if (CONFIG.LOG.PRETTIFY_SQL) { | 57 | if (CONFIG.LOG.PRETTIFY_SQL) { |
58 | additionalInfos += '\n' + sqlFormat(info.sql, { | 58 | additionalInfos += '\n' + sqlFormat(info.sql, { |
59 | language: 'sql', | 59 | language: 'sql', |
60 | indent: ' ' | 60 | tabWidth: 2 |
61 | }) | 61 | }) |
62 | } else { | 62 | } else { |
63 | additionalInfos += ' - ' + info.sql | 63 | additionalInfos += ' - ' + info.sql |
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index a9869e987..495e83558 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -1,11 +1,8 @@ | |||
1 | import { createWriteStream, remove } from 'fs-extra' | 1 | import { createWriteStream, remove } from 'fs-extra' |
2 | import got, { CancelableRequest, NormalizedOptions, Options as GotOptions, RequestError, Response } from 'got' | 2 | import got, { CancelableRequest, NormalizedOptions, Options as GotOptions, RequestError, Response } from 'got' |
3 | import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent' | 3 | import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent' |
4 | import { join } from 'path' | ||
5 | import { CONFIG } from '../initializers/config' | ||
6 | import { ACTIVITY_PUB, BINARY_CONTENT_TYPES, PEERTUBE_VERSION, REQUEST_TIMEOUTS, WEBSERVER } from '../initializers/constants' | 4 | import { ACTIVITY_PUB, BINARY_CONTENT_TYPES, PEERTUBE_VERSION, REQUEST_TIMEOUTS, WEBSERVER } from '../initializers/constants' |
7 | import { pipelinePromise } from './core-utils' | 5 | import { pipelinePromise } from './core-utils' |
8 | import { processImage } from './image-utils' | ||
9 | import { logger, loggerTagsFactory } from './logger' | 6 | import { logger, loggerTagsFactory } from './logger' |
10 | import { getProxy, isProxyEnabled } from './proxy' | 7 | import { getProxy, isProxyEnabled } from './proxy' |
11 | 8 | ||
@@ -147,21 +144,6 @@ async function doRequestAndSaveToFile ( | |||
147 | } | 144 | } |
148 | } | 145 | } |
149 | 146 | ||
150 | async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { | ||
151 | const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) | ||
152 | await doRequestAndSaveToFile(url, tmpPath) | ||
153 | |||
154 | const destPath = join(destDir, destName) | ||
155 | |||
156 | try { | ||
157 | await processImage(tmpPath, destPath, size) | ||
158 | } catch (err) { | ||
159 | await remove(tmpPath) | ||
160 | |||
161 | throw err | ||
162 | } | ||
163 | } | ||
164 | |||
165 | function getAgent () { | 147 | function getAgent () { |
166 | if (!isProxyEnabled()) return {} | 148 | if (!isProxyEnabled()) return {} |
167 | 149 | ||
@@ -211,7 +193,6 @@ export { | |||
211 | doJSONRequest, | 193 | doJSONRequest, |
212 | doRequestAndSaveToFile, | 194 | doRequestAndSaveToFile, |
213 | isBinaryResponse, | 195 | isBinaryResponse, |
214 | downloadImage, | ||
215 | getAgent, | 196 | getAgent, |
216 | findLatestRedirection, | 197 | findLatestRedirection, |
217 | peertubeGot | 198 | peertubeGot |
diff --git a/server/helpers/upload.ts b/server/helpers/upload.ts index c94c7ab82..3cb17edd0 100644 --- a/server/helpers/upload.ts +++ b/server/helpers/upload.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { JobQueue } from '@server/lib/job-queue' | ||
3 | import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants' | 2 | import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants' |
4 | 3 | ||
5 | function getResumableUploadPath (filename?: string) { | 4 | function getResumableUploadPath (filename?: string) { |
@@ -8,14 +7,8 @@ function getResumableUploadPath (filename?: string) { | |||
8 | return RESUMABLE_UPLOAD_DIRECTORY | 7 | return RESUMABLE_UPLOAD_DIRECTORY |
9 | } | 8 | } |
10 | 9 | ||
11 | function scheduleDeleteResumableUploadMetaFile (filepath: string) { | ||
12 | const payload = { filepath } | ||
13 | JobQueue.Instance.createJob({ type: 'delete-resumable-upload-meta-file', payload }, { delay: 900 * 1000 }) // executed in 15 min | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
17 | 11 | ||
18 | export { | 12 | export { |
19 | getResumableUploadPath, | 13 | getResumableUploadPath |
20 | scheduleDeleteResumableUploadMetaFile | ||
21 | } | 14 | } |
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 794303743..359f0c31d 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -31,8 +31,8 @@ function checkMissedConfig () { | |||
31 | 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', | 31 | 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', |
32 | 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', | 32 | 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', |
33 | 'transcoding.resolutions.2160p', 'video_studio.enabled', | 33 | 'transcoding.resolutions.2160p', 'video_studio.enabled', |
34 | 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'auto_blacklist.videos.of_users.enabled', | 34 | 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'import.videos.timeout', |
35 | 'trending.videos.interval_days', | 35 | 'auto_blacklist.videos.of_users.enabled', 'trending.videos.interval_days', |
36 | 'client.videos.miniature.display_author_avatar', | 36 | 'client.videos.miniature.display_author_avatar', |
37 | 'client.videos.miniature.prefer_author_display_name', 'client.menu.login.redirect_on_single_external_auth', | 37 | 'client.videos.miniature.prefer_author_display_name', 'client.menu.login.redirect_on_single_external_auth', |
38 | 'defaults.publish.download_enabled', 'defaults.publish.comments_enabled', 'defaults.publish.privacy', 'defaults.publish.licence', | 38 | 'defaults.publish.download_enabled', 'defaults.publish.comments_enabled', 'defaults.publish.privacy', 'defaults.publish.licence', |
@@ -44,6 +44,7 @@ function checkMissedConfig () { | |||
44 | 'history.videos.max_age', 'views.videos.remote.max_age', 'views.videos.local_buffer_update_interval', 'views.videos.ip_view_expiration', | 44 | 'history.videos.max_age', 'views.videos.remote.max_age', 'views.videos.local_buffer_update_interval', 'views.videos.ip_view_expiration', |
45 | 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max', | 45 | 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max', |
46 | 'theme.default', | 46 | 'theme.default', |
47 | 'feeds.videos.count', 'feeds.comments.count', | ||
47 | 'geo_ip.enabled', 'geo_ip.country.database_url', | 48 | 'geo_ip.enabled', 'geo_ip.country.database_url', |
48 | 'remote_redundancy.videos.accept_from', | 49 | 'remote_redundancy.videos.accept_from', |
49 | 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', | 50 | 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 59a65d6a5..c76a839bc 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -247,6 +247,14 @@ const CONFIG = { | |||
247 | } | 247 | } |
248 | } | 248 | } |
249 | }, | 249 | }, |
250 | FEEDS: { | ||
251 | VIDEOS: { | ||
252 | COUNT: config.get<number>('feeds.videos.count') | ||
253 | }, | ||
254 | COMMENTS: { | ||
255 | COUNT: config.get<number>('feeds.comments.count') | ||
256 | } | ||
257 | }, | ||
250 | ADMIN: { | 258 | ADMIN: { |
251 | get EMAIL () { return config.get<string>('admin.email') } | 259 | get EMAIL () { return config.get<string>('admin.email') } |
252 | }, | 260 | }, |
@@ -349,6 +357,7 @@ const CONFIG = { | |||
349 | IMPORT: { | 357 | IMPORT: { |
350 | VIDEOS: { | 358 | VIDEOS: { |
351 | get CONCURRENCY () { return config.get<number>('import.videos.concurrency') }, | 359 | get CONCURRENCY () { return config.get<number>('import.videos.concurrency') }, |
360 | get TIMEOUT () { return parseDurationToMs(config.get<string>('import.videos.timeout')) }, | ||
352 | 361 | ||
353 | HTTP: { | 362 | HTTP: { |
354 | get ENABLED () { return config.get<boolean>('import.videos.http.enabled') }, | 363 | get ENABLED () { return config.get<boolean>('import.videos.http.enabled') }, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 88f370a9a..175935835 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
24 | 24 | ||
25 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
26 | 26 | ||
27 | const LAST_MIGRATION_VERSION = 710 | 27 | const LAST_MIGRATION_VERSION = 715 |
28 | 28 | ||
29 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
30 | 30 | ||
@@ -58,7 +58,7 @@ const WEBSERVER = { | |||
58 | 58 | ||
59 | // Sortable columns per schema | 59 | // Sortable columns per schema |
60 | const SORTABLE_COLUMNS = { | 60 | const SORTABLE_COLUMNS = { |
61 | USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ], | 61 | ADMIN_USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ], |
62 | USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ], | 62 | USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ], |
63 | ACCOUNTS: [ 'createdAt' ], | 63 | ACCOUNTS: [ 'createdAt' ], |
64 | JOBS: [ 'createdAt' ], | 64 | JOBS: [ 'createdAt' ], |
@@ -186,7 +186,7 @@ const JOB_TTL: { [id in JobType]: number } = { | |||
186 | 'video-file-import': 1000 * 3600, // 1 hour | 186 | 'video-file-import': 1000 * 3600, // 1 hour |
187 | 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long | 187 | 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long |
188 | 'video-studio-edition': 1000 * 3600 * 10, // 10 hours | 188 | 'video-studio-edition': 1000 * 3600 * 10, // 10 hours |
189 | 'video-import': 1000 * 3600 * 2, // 2 hours | 189 | 'video-import': CONFIG.IMPORT.VIDEOS.TIMEOUT, |
190 | 'email': 60000 * 10, // 10 minutes | 190 | 'email': 60000 * 10, // 10 minutes |
191 | 'actor-keys': 60000 * 20, // 20 minutes | 191 | 'actor-keys': 60000 * 20, // 20 minutes |
192 | 'videos-views-stats': undefined, // Unlimited | 192 | 'videos-views-stats': undefined, // Unlimited |
@@ -213,7 +213,7 @@ const CRAWL_REQUEST_CONCURRENCY = 1 // How many requests in parallel to fetch re | |||
213 | 213 | ||
214 | const AP_CLEANER = { | 214 | const AP_CLEANER = { |
215 | CONCURRENCY: 10, // How many requests in parallel we do in activitypub-cleaner job | 215 | CONCURRENCY: 10, // How many requests in parallel we do in activitypub-cleaner job |
216 | UNAVAILABLE_TRESHOLD: 3, // How many attemps we do before removing an unavailable remote resource | 216 | UNAVAILABLE_TRESHOLD: 3, // How many attempts we do before removing an unavailable remote resource |
217 | PERIOD: parseDurationToMs('1 week') // /!\ Has to be sync with REPEAT_JOBS | 217 | PERIOD: parseDurationToMs('1 week') // /!\ Has to be sync with REPEAT_JOBS |
218 | } | 218 | } |
219 | 219 | ||
@@ -734,16 +734,21 @@ const VIDEO_LIVE = { | |||
734 | const MEMOIZE_TTL = { | 734 | const MEMOIZE_TTL = { |
735 | OVERVIEWS_SAMPLE: 1000 * 3600 * 4, // 4 hours | 735 | OVERVIEWS_SAMPLE: 1000 * 3600 * 4, // 4 hours |
736 | INFO_HASH_EXISTS: 1000 * 3600 * 12, // 12 hours | 736 | INFO_HASH_EXISTS: 1000 * 3600 * 12, // 12 hours |
737 | VIDEO_DURATION: 1000 * 10, // 10 seconds | ||
737 | LIVE_ABLE_TO_UPLOAD: 1000 * 60, // 1 minute | 738 | LIVE_ABLE_TO_UPLOAD: 1000 * 60, // 1 minute |
738 | LIVE_CHECK_SOCKET_HEALTH: 1000 * 60 // 1 minute | 739 | LIVE_CHECK_SOCKET_HEALTH: 1000 * 60 // 1 minute |
739 | } | 740 | } |
740 | 741 | ||
741 | const MEMOIZE_LENGTH = { | 742 | const MEMOIZE_LENGTH = { |
742 | INFO_HASH_EXISTS: 200 | 743 | INFO_HASH_EXISTS: 200, |
744 | VIDEO_DURATION: 200 | ||
743 | } | 745 | } |
744 | 746 | ||
745 | const QUEUE_CONCURRENCY = { | 747 | const WORKER_THREADS = { |
746 | ACTOR_PROCESS_IMAGE: 3 | 748 | DOWNLOAD_IMAGE: { |
749 | CONCURRENCY: 3, | ||
750 | MAX_THREADS: 1 | ||
751 | } | ||
747 | } | 752 | } |
748 | 753 | ||
749 | const REDUNDANCY = { | 754 | const REDUNDANCY = { |
@@ -769,12 +774,6 @@ const CUSTOM_HTML_TAG_COMMENTS = { | |||
769 | SERVER_CONFIG: '<!-- server config -->' | 774 | SERVER_CONFIG: '<!-- server config -->' |
770 | } | 775 | } |
771 | 776 | ||
772 | // --------------------------------------------------------------------------- | ||
773 | |||
774 | const FEEDS = { | ||
775 | COUNT: 20 | ||
776 | } | ||
777 | |||
778 | const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000 | 777 | const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000 |
779 | const LOG_FILENAME = 'peertube.log' | 778 | const LOG_FILENAME = 'peertube.log' |
780 | const AUDIT_LOG_FILENAME = 'peertube-audit.log' | 779 | const AUDIT_LOG_FILENAME = 'peertube-audit.log' |
@@ -818,7 +817,7 @@ const STATS_TIMESERIE = { | |||
818 | // --------------------------------------------------------------------------- | 817 | // --------------------------------------------------------------------------- |
819 | 818 | ||
820 | // Special constants for a test instance | 819 | // Special constants for a test instance |
821 | if (isTestInstance() === true) { | 820 | if (isTestInstance() === true && process.env.PRODUCTION_CONSTANTS !== 'true') { |
822 | PRIVATE_RSA_KEY_SIZE = 1024 | 821 | PRIVATE_RSA_KEY_SIZE = 1024 |
823 | 822 | ||
824 | ACTOR_FOLLOW_SCORE.BASE = 20 | 823 | ACTOR_FOLLOW_SCORE.BASE = 20 |
@@ -942,7 +941,6 @@ export { | |||
942 | ROUTE_CACHE_LIFETIME, | 941 | ROUTE_CACHE_LIFETIME, |
943 | SORTABLE_COLUMNS, | 942 | SORTABLE_COLUMNS, |
944 | HLS_STREAMING_PLAYLIST_DIRECTORY, | 943 | HLS_STREAMING_PLAYLIST_DIRECTORY, |
945 | FEEDS, | ||
946 | JOB_TTL, | 944 | JOB_TTL, |
947 | DEFAULT_THEME_NAME, | 945 | DEFAULT_THEME_NAME, |
948 | NSFW_POLICY_TYPES, | 946 | NSFW_POLICY_TYPES, |
@@ -960,7 +958,7 @@ export { | |||
960 | VIDEO_PRIVACIES, | 958 | VIDEO_PRIVACIES, |
961 | VIDEO_LICENCES, | 959 | VIDEO_LICENCES, |
962 | VIDEO_STATES, | 960 | VIDEO_STATES, |
963 | QUEUE_CONCURRENCY, | 961 | WORKER_THREADS, |
964 | VIDEO_RATE_TYPES, | 962 | VIDEO_RATE_TYPES, |
965 | JOB_PRIORITY, | 963 | JOB_PRIORITY, |
966 | VIDEO_TRANSCODING_FPS, | 964 | VIDEO_TRANSCODING_FPS, |
@@ -1163,6 +1161,7 @@ function buildLanguages () { | |||
1163 | // Override Occitan label | 1161 | // Override Occitan label |
1164 | languages['oc'] = 'Occitan' | 1162 | languages['oc'] = 'Occitan' |
1165 | languages['el'] = 'Greek' | 1163 | languages['el'] = 'Greek' |
1164 | languages['tok'] = 'Toki Pona' | ||
1166 | 1165 | ||
1167 | // Chinese languages | 1166 | // Chinese languages |
1168 | languages['zh-Hans'] = 'Simplified Chinese' | 1167 | languages['zh-Hans'] = 'Simplified Chinese' |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 3576f444c..09786a91f 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -49,6 +49,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla | |||
49 | import { VideoTagModel } from '../models/video/video-tag' | 49 | import { VideoTagModel } from '../models/video/video-tag' |
50 | import { VideoViewModel } from '../models/view/video-view' | 50 | import { VideoViewModel } from '../models/view/video-view' |
51 | import { CONFIG } from './config' | 51 | import { CONFIG } from './config' |
52 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
52 | 53 | ||
53 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 54 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
54 | 55 | ||
@@ -126,6 +127,7 @@ async function initDatabaseModels (silent: boolean) { | |||
126 | VideoChannelModel, | 127 | VideoChannelModel, |
127 | VideoShareModel, | 128 | VideoShareModel, |
128 | VideoFileModel, | 129 | VideoFileModel, |
130 | VideoSourceModel, | ||
129 | VideoCaptionModel, | 131 | VideoCaptionModel, |
130 | VideoBlacklistModel, | 132 | VideoBlacklistModel, |
131 | VideoTagModel, | 133 | VideoTagModel, |
diff --git a/server/initializers/migrations/0715-video-source.ts b/server/initializers/migrations/0715-video-source.ts new file mode 100644 index 000000000..efcf77ebd --- /dev/null +++ b/server/initializers/migrations/0715-video-source.ts | |||
@@ -0,0 +1,34 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const query = ` | ||
11 | CREATE TABLE IF NOT EXISTS "videoSource" ( | ||
12 | "id" SERIAL , | ||
13 | "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
14 | "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
15 | "filename" VARCHAR(255) DEFAULT NULL, | ||
16 | "videoId" INTEGER | ||
17 | REFERENCES "video" ("id") | ||
18 | ON DELETE CASCADE | ||
19 | ON UPDATE CASCADE, | ||
20 | PRIMARY KEY ("id") | ||
21 | ); | ||
22 | ` | ||
23 | await utils.sequelize.query(query) | ||
24 | } | ||
25 | } | ||
26 | |||
27 | function down (options) { | ||
28 | throw new Error('Not implemented.') | ||
29 | } | ||
30 | |||
31 | export { | ||
32 | up, | ||
33 | down | ||
34 | } | ||
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 3e7931bb2..76ed37aae 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -124,7 +124,7 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: MAc | |||
124 | return | 124 | return |
125 | } | 125 | } |
126 | 126 | ||
127 | // Try to not forward unwanted commments on our videos | 127 | // Try to not forward unwanted comments on our videos |
128 | if (video.isOwned()) { | 128 | if (video.isOwned()) { |
129 | if (await isBlockedByServerOrAccount(comment.Account, video.VideoChannel.Account)) { | 129 | if (await isBlockedByServerOrAccount(comment.Account, video.VideoChannel.Account)) { |
130 | logger.info('Skip comment forward from blocked account or server %s.', comment.Account.Actor.url) | 130 | logger.info('Skip comment forward from blocked account or server %s.', comment.Account.Actor.url) |
diff --git a/server/lib/activitypub/videos/shared/creator.ts b/server/lib/activitypub/videos/shared/creator.ts index 688bcbb53..07252fea2 100644 --- a/server/lib/activitypub/videos/shared/creator.ts +++ b/server/lib/activitypub/videos/shared/creator.ts | |||
@@ -24,7 +24,7 @@ export class APVideoCreator extends APVideoAbstractBuilder { | |||
24 | const channel = channelActor.VideoChannel | 24 | const channel = channelActor.VideoChannel |
25 | 25 | ||
26 | const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to) | 26 | const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to) |
27 | const video = VideoModel.build(videoData) as MVideoThumbnail | 27 | const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail |
28 | 28 | ||
29 | const promiseThumbnail = this.tryToGenerateThumbnail(video) | 29 | const promiseThumbnail = this.tryToGenerateThumbnail(video) |
30 | 30 | ||
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts index f02b9cba6..86699c5b8 100644 --- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts +++ b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts | |||
@@ -210,8 +210,6 @@ function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: Vi | |||
210 | 210 | ||
211 | updatedAt: new Date(videoObject.updated), | 211 | updatedAt: new Date(videoObject.updated), |
212 | views: videoObject.views, | 212 | views: videoObject.views, |
213 | likes: 0, | ||
214 | dislikes: 0, | ||
215 | remote: true, | 213 | remote: true, |
216 | privacy | 214 | privacy |
217 | } | 215 | } |
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 337364ac9..1e8d03023 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -30,6 +30,7 @@ import { MAccountActor, MChannelActor } from '../types/models' | |||
30 | import { getActivityStreamDuration } from './activitypub/activity' | 30 | import { getActivityStreamDuration } from './activitypub/activity' |
31 | import { getBiggestActorImage } from './actor-image' | 31 | import { getBiggestActorImage } from './actor-image' |
32 | import { ServerConfigManager } from './server-config-manager' | 32 | import { ServerConfigManager } from './server-config-manager' |
33 | import { isTestInstance } from '@server/helpers/core-utils' | ||
33 | 34 | ||
34 | type Tags = { | 35 | type Tags = { |
35 | ogType: string | 36 | ogType: string |
@@ -232,7 +233,10 @@ class ClientHtml { | |||
232 | static async getEmbedHTML () { | 233 | static async getEmbedHTML () { |
233 | const path = ClientHtml.getEmbedPath() | 234 | const path = ClientHtml.getEmbedPath() |
234 | 235 | ||
235 | if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] | 236 | // Disable HTML cache in dev mode because webpack can regenerate JS files |
237 | if (!isTestInstance() && ClientHtml.htmlCache[path]) { | ||
238 | return ClientHtml.htmlCache[path] | ||
239 | } | ||
236 | 240 | ||
237 | const buffer = await readFile(path) | 241 | const buffer = await readFile(path) |
238 | const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig() | 242 | const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig() |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index aebca04fe..edc99057c 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -179,7 +179,7 @@ class Emailer { | |||
179 | } | 179 | } |
180 | } | 180 | } |
181 | 181 | ||
182 | // overriden/new variables given for a specific template in the payload | 182 | // overridden/new variables given for a specific template in the payload |
183 | const sendOptions = merge(baseOptions, options) | 183 | const sendOptions = merge(baseOptions, options) |
184 | 184 | ||
185 | await email.send(sendOptions) | 185 | await email.send(sendOptions) |
diff --git a/server/lib/job-queue/handlers/move-to-object-storage.ts b/server/lib/job-queue/handlers/move-to-object-storage.ts index f480b32cd..49064052c 100644 --- a/server/lib/job-queue/handlers/move-to-object-storage.ts +++ b/server/lib/job-queue/handlers/move-to-object-storage.ts | |||
@@ -48,15 +48,24 @@ export async function processMoveToObjectStorage (job: Job) { | |||
48 | await doAfterLastJob({ video, previousVideoState: payload.previousVideoState, isNewVideo: payload.isNewVideo }) | 48 | await doAfterLastJob({ video, previousVideoState: payload.previousVideoState, isNewVideo: payload.isNewVideo }) |
49 | } | 49 | } |
50 | } catch (err) { | 50 | } catch (err) { |
51 | logger.error('Cannot move video %s to object storage.', video.url, { err, ...lTags }) | 51 | await onMoveToObjectStorageFailure(job, err) |
52 | |||
53 | await moveToFailedMoveToObjectStorageState(video) | ||
54 | await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingMove') | ||
55 | } | 52 | } |
56 | 53 | ||
57 | return payload.videoUUID | 54 | return payload.videoUUID |
58 | } | 55 | } |
59 | 56 | ||
57 | export async function onMoveToObjectStorageFailure (job: Job, err: any) { | ||
58 | const payload = job.data as MoveObjectStoragePayload | ||
59 | |||
60 | const video = await VideoModel.loadWithFiles(payload.videoUUID) | ||
61 | if (!video) return | ||
62 | |||
63 | logger.error('Cannot move video %s to object storage.', video.url, { err, ...lTagsBase(video.uuid, video.url) }) | ||
64 | |||
65 | await moveToFailedMoveToObjectStorageState(video) | ||
66 | await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingMove') | ||
67 | } | ||
68 | |||
60 | // --------------------------------------------------------------------------- | 69 | // --------------------------------------------------------------------------- |
61 | 70 | ||
62 | async function moveWebTorrentFiles (video: MVideoWithAllFiles) { | 71 | async function moveWebTorrentFiles (video: MVideoWithAllFiles) { |
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index f339e9135..ce24763f1 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -33,7 +33,7 @@ import { refreshAPObject } from './handlers/activitypub-refresher' | |||
33 | import { processActorKeys } from './handlers/actor-keys' | 33 | import { processActorKeys } from './handlers/actor-keys' |
34 | import { processEmail } from './handlers/email' | 34 | import { processEmail } from './handlers/email' |
35 | import { processManageVideoTorrent } from './handlers/manage-video-torrent' | 35 | import { processManageVideoTorrent } from './handlers/manage-video-torrent' |
36 | import { processMoveToObjectStorage } from './handlers/move-to-object-storage' | 36 | import { onMoveToObjectStorageFailure, processMoveToObjectStorage } from './handlers/move-to-object-storage' |
37 | import { processVideoFileImport } from './handlers/video-file-import' | 37 | import { processVideoFileImport } from './handlers/video-file-import' |
38 | import { processVideoImport } from './handlers/video-import' | 38 | import { processVideoImport } from './handlers/video-import' |
39 | import { processVideoLiveEnding } from './handlers/video-live-ending' | 39 | import { processVideoLiveEnding } from './handlers/video-live-ending' |
@@ -88,6 +88,10 @@ const handlers: { [id in JobType]: (job: Job) => Promise<any> } = { | |||
88 | 'video-studio-edition': processVideoStudioEdition | 88 | 'video-studio-edition': processVideoStudioEdition |
89 | } | 89 | } |
90 | 90 | ||
91 | const errorHandlers: { [id in JobType]?: (job: Job, err: any) => Promise<any> } = { | ||
92 | 'move-to-object-storage': onMoveToObjectStorageFailure | ||
93 | } | ||
94 | |||
91 | const jobTypes: JobType[] = [ | 95 | const jobTypes: JobType[] = [ |
92 | 'activitypub-follow', | 96 | 'activitypub-follow', |
93 | 'activitypub-http-broadcast', | 97 | 'activitypub-http-broadcast', |
@@ -162,6 +166,11 @@ class JobQueue { | |||
162 | : 'error' | 166 | : 'error' |
163 | 167 | ||
164 | logger.log(logLevel, 'Cannot execute job %d in queue %s.', job.id, handlerName, { payload: job.data, err }) | 168 | logger.log(logLevel, 'Cannot execute job %d in queue %s.', job.id, handlerName, { payload: job.data, err }) |
169 | |||
170 | if (errorHandlers[job.name]) { | ||
171 | errorHandlers[job.name](job, err) | ||
172 | .catch(err => logger.error('Cannot run error handler for job failure %d in queue %s.', job.id, handlerName, { err })) | ||
173 | } | ||
165 | }) | 174 | }) |
166 | 175 | ||
167 | queue.on('error', err => { | 176 | queue.on('error', err => { |
diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts index 01046d017..e3b04c094 100644 --- a/server/lib/local-actor.ts +++ b/server/lib/local-actor.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | import { queue } from 'async' | ||
2 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
3 | import LRUCache from 'lru-cache' | 2 | import LRUCache from 'lru-cache' |
4 | import { join } from 'path' | 3 | import { join } from 'path' |
@@ -8,13 +7,13 @@ import { buildUUID } from '@shared/extra-utils' | |||
8 | import { ActivityPubActorType, ActorImageType } from '@shared/models' | 7 | import { ActivityPubActorType, ActorImageType } from '@shared/models' |
9 | import { retryTransactionWrapper } from '../helpers/database-utils' | 8 | import { retryTransactionWrapper } from '../helpers/database-utils' |
10 | import { processImage } from '../helpers/image-utils' | 9 | import { processImage } from '../helpers/image-utils' |
11 | import { downloadImage } from '../helpers/requests' | ||
12 | import { CONFIG } from '../initializers/config' | 10 | import { CONFIG } from '../initializers/config' |
13 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants' | 11 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants' |
14 | import { sequelizeTypescript } from '../initializers/database' | 12 | import { sequelizeTypescript } from '../initializers/database' |
15 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' | 13 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' |
16 | import { deleteActorImages, updateActorImages } from './activitypub/actors' | 14 | import { deleteActorImages, updateActorImages } from './activitypub/actors' |
17 | import { sendUpdateActor } from './activitypub/send' | 15 | import { sendUpdateActor } from './activitypub/send' |
16 | import { downloadImageFromWorker } from './worker/parent-process' | ||
18 | 17 | ||
19 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { | 18 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { |
20 | return new ActorModel({ | 19 | return new ActorModel({ |
@@ -87,27 +86,22 @@ async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MC | |||
87 | }) | 86 | }) |
88 | } | 87 | } |
89 | 88 | ||
90 | type DownloadImageQueueTask = { | 89 | // --------------------------------------------------------------------------- |
90 | |||
91 | function downloadActorImageFromWorker (options: { | ||
91 | fileUrl: string | 92 | fileUrl: string |
92 | filename: string | 93 | filename: string |
93 | type: ActorImageType | 94 | type: ActorImageType |
94 | size: typeof ACTOR_IMAGES_SIZE[ActorImageType][0] | 95 | size: typeof ACTOR_IMAGES_SIZE[ActorImageType][0] |
95 | } | 96 | }) { |
96 | 97 | const downloaderOptions = { | |
97 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | 98 | url: options.fileUrl, |
98 | downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, task.size) | 99 | destDir: CONFIG.STORAGE.ACTOR_IMAGES, |
99 | .then(() => cb()) | 100 | destName: options.filename, |
100 | .catch(err => cb(err)) | 101 | size: options.size |
101 | }, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) | 102 | } |
102 | |||
103 | function pushActorImageProcessInQueue (task: DownloadImageQueueTask) { | ||
104 | return new Promise<void>((res, rej) => { | ||
105 | downloadImageQueue.push(task, err => { | ||
106 | if (err) return rej(err) | ||
107 | 103 | ||
108 | return res() | 104 | return downloadImageFromWorker(downloaderOptions) |
109 | }) | ||
110 | }) | ||
111 | } | 105 | } |
112 | 106 | ||
113 | // Unsafe so could returns paths that does not exist anymore | 107 | // Unsafe so could returns paths that does not exist anymore |
@@ -116,7 +110,8 @@ const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE. | |||
116 | export { | 110 | export { |
117 | actorImagePathUnsafeCache, | 111 | actorImagePathUnsafeCache, |
118 | updateLocalActorImageFiles, | 112 | updateLocalActorImageFiles, |
113 | downloadActorImageFromWorker, | ||
119 | deleteLocalActorImageFile, | 114 | deleteLocalActorImageFile, |
120 | pushActorImageProcessInQueue, | 115 | downloadImageFromWorker, |
121 | buildActorInstance | 116 | buildActorInstance |
122 | } | 117 | } |
diff --git a/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts b/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts index daefa25bd..a7292de69 100644 --- a/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts +++ b/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts | |||
@@ -5,7 +5,7 @@ import { MAbuseFull, MAbuseMessage, MAccountDefault, MUserWithNotificationSettin | |||
5 | import { UserNotificationType } from '@shared/models' | 5 | import { UserNotificationType } from '@shared/models' |
6 | import { AbstractNotification } from '../common/abstract-notification' | 6 | import { AbstractNotification } from '../common/abstract-notification' |
7 | 7 | ||
8 | export type NewAbuseMessagePayload = { | 8 | type NewAbuseMessagePayload = { |
9 | abuse: MAbuseFull | 9 | abuse: MAbuseFull |
10 | message: MAbuseMessage | 10 | message: MAbuseMessage |
11 | } | 11 | } |
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index d052de786..d6d053d2f 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { createClient, RedisClientOptions, RedisModules, RedisScripts } from 'redis' | 1 | import { createClient, RedisClientOptions, RedisModules } from 'redis' |
2 | import { exists } from '@server/helpers/custom-validators/misc' | 2 | import { exists } from '@server/helpers/custom-validators/misc' |
3 | import { sha256 } from '@shared/extra-utils' | 3 | import { sha256 } from '@shared/extra-utils' |
4 | import { logger } from '../helpers/logger' | 4 | import { logger } from '../helpers/logger' |
@@ -16,16 +16,12 @@ import { | |||
16 | WEBSERVER | 16 | WEBSERVER |
17 | } from '../initializers/constants' | 17 | } from '../initializers/constants' |
18 | 18 | ||
19 | // Only used for typings | ||
20 | // TODO: remove when https://github.com/microsoft/TypeScript/issues/37181 is fixed | ||
21 | const redisClientWrapperForType = () => createClient<{}, RedisScripts>() | ||
22 | |||
23 | class Redis { | 19 | class Redis { |
24 | 20 | ||
25 | private static instance: Redis | 21 | private static instance: Redis |
26 | private initialized = false | 22 | private initialized = false |
27 | private connected = false | 23 | private connected = false |
28 | private client: ReturnType<typeof redisClientWrapperForType> | 24 | private client: ReturnType<typeof createClient> |
29 | private prefix: string | 25 | private prefix: string |
30 | 26 | ||
31 | private constructor () { | 27 | private constructor () { |
@@ -308,7 +304,7 @@ class Redis { | |||
308 | return this.deleteKey('resumable-upload-' + uploadId) | 304 | return this.deleteKey('resumable-upload-' + uploadId) |
309 | } | 305 | } |
310 | 306 | ||
311 | /* ************ AP ressource unavailability ************ */ | 307 | /* ************ AP resource unavailability ************ */ |
312 | 308 | ||
313 | async addAPUnavailability (url: string) { | 309 | async addAPUnavailability (url: string) { |
314 | const key = this.generateAPUnavailabilityKey(url) | 310 | const key = this.generateAPUnavailabilityKey(url) |
diff --git a/server/lib/schedulers/geo-ip-update-scheduler.ts b/server/lib/schedulers/geo-ip-update-scheduler.ts index 9dda6d76c..b06f5a9b5 100644 --- a/server/lib/schedulers/geo-ip-update-scheduler.ts +++ b/server/lib/schedulers/geo-ip-update-scheduler.ts | |||
@@ -6,7 +6,7 @@ export class GeoIPUpdateScheduler extends AbstractScheduler { | |||
6 | 6 | ||
7 | private static instance: AbstractScheduler | 7 | private static instance: AbstractScheduler |
8 | 8 | ||
9 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.YOUTUBE_DL_UPDATE | 9 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.GEO_IP_UPDATE |
10 | 10 | ||
11 | private constructor () { | 11 | private constructor () { |
12 | super() | 12 | super() |
diff --git a/server/lib/signup.ts b/server/lib/signup.ts index 3c1397a12..f094531eb 100644 --- a/server/lib/signup.ts +++ b/server/lib/signup.ts | |||
@@ -26,7 +26,7 @@ function isSignupAllowedForCurrentIP (ip: string) { | |||
26 | const excludeList = [ 'blacklist' ] | 26 | const excludeList = [ 'blacklist' ] |
27 | let matched = '' | 27 | let matched = '' |
28 | 28 | ||
29 | // if there is a valid, non-empty whitelist, we exclude all unknown adresses too | 29 | // if there is a valid, non-empty whitelist, we exclude all unknown addresses too |
30 | if (CONFIG.SIGNUP.FILTERS.CIDR.WHITELIST.filter(cidr => isCidr(cidr)).length > 0) { | 30 | if (CONFIG.SIGNUP.FILTERS.CIDR.WHITELIST.filter(cidr => isCidr(cidr)).length > 0) { |
31 | excludeList.push('unknown') | 31 | excludeList.push('unknown') |
32 | } | 32 | } |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index aa2d7a813..f00c87623 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { ThumbnailType } from '@shared/models' | 2 | import { ThumbnailType } from '@shared/models' |
3 | import { generateImageFilename, generateImageFromVideoFile, processImage } from '../helpers/image-utils' | 3 | import { generateImageFilename, generateImageFromVideoFile, processImage } from '../helpers/image-utils' |
4 | import { downloadImage } from '../helpers/requests' | ||
5 | import { CONFIG } from '../initializers/config' | 4 | import { CONFIG } from '../initializers/config' |
6 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' | 5 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' |
7 | import { ThumbnailModel } from '../models/video/thumbnail' | 6 | import { ThumbnailModel } from '../models/video/thumbnail' |
8 | import { MVideoFile, MVideoThumbnail, MVideoUUID } from '../types/models' | 7 | import { MVideoFile, MVideoThumbnail, MVideoUUID } from '../types/models' |
9 | import { MThumbnail } from '../types/models/video/thumbnail' | 8 | import { MThumbnail } from '../types/models/video/thumbnail' |
10 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' | 9 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' |
10 | import { downloadImageFromWorker } from './local-actor' | ||
11 | import { VideoPathManager } from './video-path-manager' | 11 | import { VideoPathManager } from './video-path-manager' |
12 | 12 | ||
13 | type ImageSize = { height?: number, width?: number } | 13 | type ImageSize = { height?: number, width?: number } |
@@ -49,7 +49,10 @@ function updatePlaylistMiniatureFromUrl (options: { | |||
49 | ? null | 49 | ? null |
50 | : downloadUrl | 50 | : downloadUrl |
51 | 51 | ||
52 | const thumbnailCreator = () => downloadImage(downloadUrl, basePath, filename, { width, height }) | 52 | const thumbnailCreator = () => { |
53 | return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } }) | ||
54 | } | ||
55 | |||
53 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) | 56 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) |
54 | } | 57 | } |
55 | 58 | ||
@@ -75,7 +78,9 @@ function updateVideoMiniatureFromUrl (options: { | |||
75 | : existingThumbnail.filename | 78 | : existingThumbnail.filename |
76 | 79 | ||
77 | const thumbnailCreator = () => { | 80 | const thumbnailCreator = () => { |
78 | if (thumbnailUrlChanged) return downloadImage(downloadUrl, basePath, filename, { width, height }) | 81 | if (thumbnailUrlChanged) { |
82 | return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } }) | ||
83 | } | ||
79 | 84 | ||
80 | return Promise.resolve() | 85 | return Promise.resolve() |
81 | } | 86 | } |
diff --git a/server/lib/video.ts b/server/lib/video.ts index a98e45c60..86718abbe 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { UploadFiles } from 'express' | 1 | import { UploadFiles } from 'express' |
2 | import { Transaction } from 'sequelize/types' | 2 | import { Transaction } from 'sequelize/types' |
3 | import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY } from '@server/initializers/constants' | 3 | import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY, MEMOIZE_LENGTH, MEMOIZE_TTL } from '@server/initializers/constants' |
4 | import { TagModel } from '@server/models/video/tag' | 4 | import { TagModel } from '@server/models/video/tag' |
5 | import { VideoModel } from '@server/models/video/video' | 5 | import { VideoModel } from '@server/models/video/video' |
6 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | 6 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' |
@@ -10,6 +10,7 @@ import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState, VideoTranscodingP | |||
10 | import { CreateJobOptions, JobQueue } from './job-queue/job-queue' | 10 | import { CreateJobOptions, JobQueue } from './job-queue/job-queue' |
11 | import { updateVideoMiniatureFromExisting } from './thumbnail' | 11 | import { updateVideoMiniatureFromExisting } from './thumbnail' |
12 | import { CONFIG } from '@server/initializers/config' | 12 | import { CONFIG } from '@server/initializers/config' |
13 | import memoizee from 'memoizee' | ||
13 | 14 | ||
14 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { | 15 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { |
15 | return { | 16 | return { |
@@ -150,6 +151,24 @@ async function addMoveToObjectStorageJob (options: { | |||
150 | 151 | ||
151 | // --------------------------------------------------------------------------- | 152 | // --------------------------------------------------------------------------- |
152 | 153 | ||
154 | async function getVideoDuration (videoId: number | string) { | ||
155 | const video = await VideoModel.load(videoId) | ||
156 | |||
157 | const duration = video.isLive | ||
158 | ? undefined | ||
159 | : video.duration | ||
160 | |||
161 | return { duration, isLive: video.isLive } | ||
162 | } | ||
163 | |||
164 | const getCachedVideoDuration = memoizee(getVideoDuration, { | ||
165 | promise: true, | ||
166 | max: MEMOIZE_LENGTH.VIDEO_DURATION, | ||
167 | maxAge: MEMOIZE_TTL.VIDEO_DURATION | ||
168 | }) | ||
169 | |||
170 | // --------------------------------------------------------------------------- | ||
171 | |||
153 | export { | 172 | export { |
154 | buildLocalVideoFromReq, | 173 | buildLocalVideoFromReq, |
155 | buildVideoThumbnailsFromReq, | 174 | buildVideoThumbnailsFromReq, |
@@ -157,5 +176,6 @@ export { | |||
157 | addOptimizeOrMergeAudioJob, | 176 | addOptimizeOrMergeAudioJob, |
158 | addTranscodingJob, | 177 | addTranscodingJob, |
159 | addMoveToObjectStorageJob, | 178 | addMoveToObjectStorageJob, |
160 | getTranscodingJobPriority | 179 | getTranscodingJobPriority, |
180 | getCachedVideoDuration | ||
161 | } | 181 | } |
diff --git a/server/lib/views/shared/video-viewer-counters.ts b/server/lib/views/shared/video-viewer-counters.ts index 5158f8f93..cf3fa5882 100644 --- a/server/lib/views/shared/video-viewer-counters.ts +++ b/server/lib/views/shared/video-viewer-counters.ts | |||
@@ -5,7 +5,7 @@ import { sendView } from '@server/lib/activitypub/send/send-view' | |||
5 | import { PeerTubeSocket } from '@server/lib/peertube-socket' | 5 | import { PeerTubeSocket } from '@server/lib/peertube-socket' |
6 | import { getServerActor } from '@server/models/application/application' | 6 | import { getServerActor } from '@server/models/application/application' |
7 | import { VideoModel } from '@server/models/video/video' | 7 | import { VideoModel } from '@server/models/video/video' |
8 | import { MVideo } from '@server/types/models' | 8 | import { MVideo, MVideoImmutable } from '@server/types/models' |
9 | import { buildUUID, sha256 } from '@shared/extra-utils' | 9 | import { buildUUID, sha256 } from '@shared/extra-utils' |
10 | 10 | ||
11 | const lTags = loggerTagsFactory('views') | 11 | const lTags = loggerTagsFactory('views') |
@@ -33,7 +33,7 @@ export class VideoViewerCounters { | |||
33 | // --------------------------------------------------------------------------- | 33 | // --------------------------------------------------------------------------- |
34 | 34 | ||
35 | async addLocalViewer (options: { | 35 | async addLocalViewer (options: { |
36 | video: MVideo | 36 | video: MVideoImmutable |
37 | ip: string | 37 | ip: string |
38 | }) { | 38 | }) { |
39 | const { video, ip } = options | 39 | const { video, ip } = options |
@@ -86,7 +86,7 @@ export class VideoViewerCounters { | |||
86 | // --------------------------------------------------------------------------- | 86 | // --------------------------------------------------------------------------- |
87 | 87 | ||
88 | private async addViewerToVideo (options: { | 88 | private async addViewerToVideo (options: { |
89 | video: MVideo | 89 | video: MVideoImmutable |
90 | viewerId: string | 90 | viewerId: string |
91 | viewerExpires?: Date | 91 | viewerExpires?: Date |
92 | }) { | 92 | }) { |
@@ -162,7 +162,7 @@ export class VideoViewerCounters { | |||
162 | return sha256(this.salt + '-' + ip + '-' + videoUUID) | 162 | return sha256(this.salt + '-' + ip + '-' + videoUUID) |
163 | } | 163 | } |
164 | 164 | ||
165 | private async federateViewerIfNeeded (video: MVideo, viewer: Viewer) { | 165 | private async federateViewerIfNeeded (video: MVideoImmutable, viewer: Viewer) { |
166 | // Federate the viewer if it's been a "long" time we did not | 166 | // Federate the viewer if it's been a "long" time we did not |
167 | const now = new Date().getTime() | 167 | const now = new Date().getTime() |
168 | const federationLimit = now - (VIEW_LIFETIME.VIEWER_COUNTER * 0.75) | 168 | const federationLimit = now - (VIEW_LIFETIME.VIEWER_COUNTER * 0.75) |
diff --git a/server/lib/views/shared/video-viewer-stats.ts b/server/lib/views/shared/video-viewer-stats.ts index a9ba25b47..a56c20559 100644 --- a/server/lib/views/shared/video-viewer-stats.ts +++ b/server/lib/views/shared/video-viewer-stats.ts | |||
@@ -10,7 +10,7 @@ import { Redis } from '@server/lib/redis' | |||
10 | import { VideoModel } from '@server/models/video/video' | 10 | import { VideoModel } from '@server/models/video/video' |
11 | import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer' | 11 | import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer' |
12 | import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section' | 12 | import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section' |
13 | import { MVideo } from '@server/types/models' | 13 | import { MVideo, MVideoImmutable } from '@server/types/models' |
14 | import { VideoViewEvent } from '@shared/models' | 14 | import { VideoViewEvent } from '@shared/models' |
15 | 15 | ||
16 | const lTags = loggerTagsFactory('views') | 16 | const lTags = loggerTagsFactory('views') |
@@ -41,7 +41,7 @@ export class VideoViewerStats { | |||
41 | // --------------------------------------------------------------------------- | 41 | // --------------------------------------------------------------------------- |
42 | 42 | ||
43 | async addLocalViewer (options: { | 43 | async addLocalViewer (options: { |
44 | video: MVideo | 44 | video: MVideoImmutable |
45 | currentTime: number | 45 | currentTime: number |
46 | ip: string | 46 | ip: string |
47 | viewEvent?: VideoViewEvent | 47 | viewEvent?: VideoViewEvent |
@@ -64,7 +64,7 @@ export class VideoViewerStats { | |||
64 | // --------------------------------------------------------------------------- | 64 | // --------------------------------------------------------------------------- |
65 | 65 | ||
66 | private async updateLocalViewerStats (options: { | 66 | private async updateLocalViewerStats (options: { |
67 | video: MVideo | 67 | video: MVideoImmutable |
68 | ip: string | 68 | ip: string |
69 | currentTime: number | 69 | currentTime: number |
70 | viewEvent?: VideoViewEvent | 70 | viewEvent?: VideoViewEvent |
diff --git a/server/lib/views/shared/video-views.ts b/server/lib/views/shared/video-views.ts index 275f7a014..e563287e1 100644 --- a/server/lib/views/shared/video-views.ts +++ b/server/lib/views/shared/video-views.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
2 | import { sendView } from '@server/lib/activitypub/send/send-view' | 2 | import { sendView } from '@server/lib/activitypub/send/send-view' |
3 | import { getCachedVideoDuration } from '@server/lib/video' | ||
3 | import { getServerActor } from '@server/models/application/application' | 4 | import { getServerActor } from '@server/models/application/application' |
4 | import { MVideo } from '@server/types/models' | 5 | import { MVideo, MVideoImmutable } from '@server/types/models' |
5 | import { buildUUID } from '@shared/extra-utils' | 6 | import { buildUUID } from '@shared/extra-utils' |
6 | import { Redis } from '../../redis' | 7 | import { Redis } from '../../redis' |
7 | 8 | ||
@@ -10,7 +11,7 @@ const lTags = loggerTagsFactory('views') | |||
10 | export class VideoViews { | 11 | export class VideoViews { |
11 | 12 | ||
12 | async addLocalView (options: { | 13 | async addLocalView (options: { |
13 | video: MVideo | 14 | video: MVideoImmutable |
14 | ip: string | 15 | ip: string |
15 | watchTime: number | 16 | watchTime: number |
16 | }) { | 17 | }) { |
@@ -18,7 +19,7 @@ export class VideoViews { | |||
18 | 19 | ||
19 | logger.debug('Adding local view to video %s.', video.uuid, { watchTime, ...lTags(video.uuid) }) | 20 | logger.debug('Adding local view to video %s.', video.uuid, { watchTime, ...lTags(video.uuid) }) |
20 | 21 | ||
21 | if (!this.hasEnoughWatchTime(video, watchTime)) return false | 22 | if (!await this.hasEnoughWatchTime(video, watchTime)) return false |
22 | 23 | ||
23 | const viewExists = await Redis.Instance.doesVideoIPViewExist(ip, video.uuid) | 24 | const viewExists = await Redis.Instance.doesVideoIPViewExist(ip, video.uuid) |
24 | if (viewExists) return false | 25 | if (viewExists) return false |
@@ -46,7 +47,7 @@ export class VideoViews { | |||
46 | 47 | ||
47 | // --------------------------------------------------------------------------- | 48 | // --------------------------------------------------------------------------- |
48 | 49 | ||
49 | private async addView (video: MVideo) { | 50 | private async addView (video: MVideoImmutable) { |
50 | const promises: Promise<any>[] = [] | 51 | const promises: Promise<any>[] = [] |
51 | 52 | ||
52 | if (video.isOwned()) { | 53 | if (video.isOwned()) { |
@@ -58,10 +59,12 @@ export class VideoViews { | |||
58 | await Promise.all(promises) | 59 | await Promise.all(promises) |
59 | } | 60 | } |
60 | 61 | ||
61 | private hasEnoughWatchTime (video: MVideo, watchTime: number) { | 62 | private async hasEnoughWatchTime (video: MVideoImmutable, watchTime: number) { |
62 | if (video.isLive || video.duration >= 30) return watchTime >= 30 | 63 | const { duration, isLive } = await getCachedVideoDuration(video.id) |
64 | |||
65 | if (isLive || duration >= 30) return watchTime >= 30 | ||
63 | 66 | ||
64 | // Check more than 50% of the video is watched | 67 | // Check more than 50% of the video is watched |
65 | return video.duration / watchTime < 2 | 68 | return duration / watchTime < 2 |
66 | } | 69 | } |
67 | } | 70 | } |
diff --git a/server/lib/views/video-views-manager.ts b/server/lib/views/video-views-manager.ts index ea3b35c6c..86758e8d8 100644 --- a/server/lib/views/video-views-manager.ts +++ b/server/lib/views/video-views-manager.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
2 | import { MVideo } from '@server/types/models' | 2 | import { MVideo, MVideoImmutable } from '@server/types/models' |
3 | import { VideoViewEvent } from '@shared/models' | 3 | import { VideoViewEvent } from '@shared/models' |
4 | import { VideoViewerCounters, VideoViewerStats, VideoViews } from './shared' | 4 | import { VideoViewerCounters, VideoViewerStats, VideoViews } from './shared' |
5 | 5 | ||
@@ -41,7 +41,7 @@ export class VideoViewsManager { | |||
41 | } | 41 | } |
42 | 42 | ||
43 | async processLocalView (options: { | 43 | async processLocalView (options: { |
44 | video: MVideo | 44 | video: MVideoImmutable |
45 | currentTime: number | 45 | currentTime: number |
46 | ip: string | null | 46 | ip: string | null |
47 | viewEvent?: VideoViewEvent | 47 | viewEvent?: VideoViewEvent |
diff --git a/server/lib/worker/parent-process.ts b/server/lib/worker/parent-process.ts new file mode 100644 index 000000000..18dabd97f --- /dev/null +++ b/server/lib/worker/parent-process.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | import { join } from 'path' | ||
2 | import Piscina from 'piscina' | ||
3 | import { WORKER_THREADS } from '@server/initializers/constants' | ||
4 | import { downloadImage } from './workers/image-downloader' | ||
5 | |||
6 | const downloadImagerWorker = new Piscina({ | ||
7 | filename: join(__dirname, 'workers', 'image-downloader.js'), | ||
8 | concurrentTasksPerWorker: WORKER_THREADS.DOWNLOAD_IMAGE.CONCURRENCY, | ||
9 | maxThreads: WORKER_THREADS.DOWNLOAD_IMAGE.MAX_THREADS | ||
10 | }) | ||
11 | |||
12 | function downloadImageFromWorker (options: Parameters<typeof downloadImage>[0]): Promise<ReturnType<typeof downloadImage>> { | ||
13 | return downloadImagerWorker.run(options) | ||
14 | } | ||
15 | |||
16 | export { | ||
17 | downloadImageFromWorker | ||
18 | } | ||
diff --git a/server/lib/worker/workers/image-downloader.ts b/server/lib/worker/workers/image-downloader.ts new file mode 100644 index 000000000..8d4a6b37e --- /dev/null +++ b/server/lib/worker/workers/image-downloader.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import { remove } from 'fs-extra' | ||
2 | import { join } from 'path' | ||
3 | import { processImage } from '@server/helpers/image-utils' | ||
4 | import { doRequestAndSaveToFile } from '@server/helpers/requests' | ||
5 | import { CONFIG } from '@server/initializers/config' | ||
6 | |||
7 | async function downloadImage (options: { | ||
8 | url: string | ||
9 | destDir: string | ||
10 | destName: string | ||
11 | size: { width: number, height: number } | ||
12 | }) { | ||
13 | const { url, destDir, destName, size } = options | ||
14 | |||
15 | const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) | ||
16 | await doRequestAndSaveToFile(url, tmpPath) | ||
17 | |||
18 | const destPath = join(destDir, destName) | ||
19 | |||
20 | try { | ||
21 | await processImage(tmpPath, destPath, size) | ||
22 | } catch (err) { | ||
23 | await remove(tmpPath) | ||
24 | |||
25 | throw err | ||
26 | } | ||
27 | } | ||
28 | |||
29 | module.exports = downloadImage | ||
30 | |||
31 | export { | ||
32 | downloadImage | ||
33 | } | ||
diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts index c5424be97..ad3b24ab2 100644 --- a/server/middlewares/auth.ts +++ b/server/middlewares/auth.ts | |||
@@ -47,7 +47,7 @@ function authenticateSocket (socket: Socket, next: (err?: any) => void) { | |||
47 | .catch(err => logger.error('Cannot get access token.', { err })) | 47 | .catch(err => logger.error('Cannot get access token.', { err })) |
48 | } | 48 | } |
49 | 49 | ||
50 | function authenticatePromiseIfNeeded (req: express.Request, res: express.Response, authenticateInQuery = false) { | 50 | function authenticatePromise (req: express.Request, res: express.Response, authenticateInQuery = false) { |
51 | return new Promise<void>(resolve => { | 51 | return new Promise<void>(resolve => { |
52 | // Already authenticated? (or tried to) | 52 | // Already authenticated? (or tried to) |
53 | if (res.locals.oauth?.token.User) return resolve() | 53 | if (res.locals.oauth?.token.User) return resolve() |
@@ -76,6 +76,6 @@ function optionalAuthenticate (req: express.Request, res: express.Response, next | |||
76 | export { | 76 | export { |
77 | authenticate, | 77 | authenticate, |
78 | authenticateSocket, | 78 | authenticateSocket, |
79 | authenticatePromiseIfNeeded, | 79 | authenticatePromise, |
80 | optionalAuthenticate | 80 | optionalAuthenticate |
81 | } | 81 | } |
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index d2ed079b6..b40f864ce 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts | |||
@@ -4,6 +4,7 @@ export * from './activitypub' | |||
4 | export * from './async' | 4 | export * from './async' |
5 | export * from './auth' | 5 | export * from './auth' |
6 | export * from './pagination' | 6 | export * from './pagination' |
7 | export * from './rate-limiter' | ||
7 | export * from './robots' | 8 | export * from './robots' |
8 | export * from './servers' | 9 | export * from './servers' |
9 | export * from './sort' | 10 | export * from './sort' |
diff --git a/server/middlewares/rate-limiter.ts b/server/middlewares/rate-limiter.ts new file mode 100644 index 000000000..bc9513969 --- /dev/null +++ b/server/middlewares/rate-limiter.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | import { UserRole } from '@shared/models' | ||
2 | import RateLimit from 'express-rate-limit' | ||
3 | import { optionalAuthenticate } from './auth' | ||
4 | |||
5 | const whitelistRoles = new Set([ UserRole.ADMINISTRATOR, UserRole.MODERATOR ]) | ||
6 | |||
7 | function buildRateLimiter (options: { | ||
8 | windowMs: number | ||
9 | max: number | ||
10 | skipFailedRequests?: boolean | ||
11 | }) { | ||
12 | return RateLimit({ | ||
13 | windowMs: options.windowMs, | ||
14 | max: options.max, | ||
15 | skipFailedRequests: options.skipFailedRequests, | ||
16 | |||
17 | handler: (req, res, next, options) => { | ||
18 | return optionalAuthenticate(req, res, () => { | ||
19 | if (res.locals.authenticated === true && whitelistRoles.has(res.locals.oauth.token.User.role)) { | ||
20 | return next() | ||
21 | } | ||
22 | |||
23 | return res.status(options.statusCode).send(options.message) | ||
24 | }) | ||
25 | } | ||
26 | }) | ||
27 | } | ||
28 | |||
29 | export { | ||
30 | buildRateLimiter | ||
31 | } | ||
diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts index f8ebaf6ed..04b4e00c9 100644 --- a/server/middlewares/validators/feeds.ts +++ b/server/middlewares/validators/feeds.ts | |||
@@ -6,6 +6,7 @@ import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helper | |||
6 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../helpers/logger' |
7 | import { | 7 | import { |
8 | areValidationErrors, | 8 | areValidationErrors, |
9 | checkCanSeeVideo, | ||
9 | doesAccountIdExist, | 10 | doesAccountIdExist, |
10 | doesAccountNameWithHostExist, | 11 | doesAccountNameWithHostExist, |
11 | doesUserFeedTokenCorrespond, | 12 | doesUserFeedTokenCorrespond, |
@@ -112,7 +113,10 @@ const videoCommentsFeedsValidator = [ | |||
112 | return res.fail({ message: 'videoId cannot be mixed with a channel filter' }) | 113 | return res.fail({ message: 'videoId cannot be mixed with a channel filter' }) |
113 | } | 114 | } |
114 | 115 | ||
115 | if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return | 116 | if (req.query.videoId) { |
117 | if (!await doesVideoExist(req.query.videoId, res)) return | ||
118 | if (!await checkCanSeeVideo({ req, res, paramId: req.query.videoId, video: res.locals.videoAll })) return | ||
119 | } | ||
116 | 120 | ||
117 | return next() | 121 | return next() |
118 | } | 122 | } |
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts index 8807435f6..2c2ae3811 100644 --- a/server/middlewares/validators/shared/videos.ts +++ b/server/middlewares/validators/shared/videos.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { Request, Response } from 'express' | 1 | import { Request, Response } from 'express' |
2 | import { isUUIDValid } from '@server/helpers/custom-validators/misc' | ||
2 | import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' | 3 | import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' |
3 | import { isAbleToUploadVideo } from '@server/lib/user' | 4 | import { isAbleToUploadVideo } from '@server/lib/user' |
4 | import { authenticatePromiseIfNeeded } from '@server/middlewares/auth' | 5 | import { authenticatePromise } from '@server/middlewares/auth' |
5 | import { VideoModel } from '@server/models/video/video' | 6 | import { VideoModel } from '@server/models/video/video' |
6 | import { VideoChannelModel } from '@server/models/video/video-channel' | 7 | import { VideoChannelModel } from '@server/models/video/video-channel' |
7 | import { VideoFileModel } from '@server/models/video/video-file' | 8 | import { VideoFileModel } from '@server/models/video/video-file' |
@@ -18,18 +19,19 @@ import { | |||
18 | MVideoThumbnail, | 19 | MVideoThumbnail, |
19 | MVideoWithRights | 20 | MVideoWithRights |
20 | } from '@server/types/models' | 21 | } from '@server/types/models' |
21 | import { HttpStatusCode, ServerErrorCode, UserRight } from '@shared/models' | 22 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models' |
22 | 23 | ||
23 | async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { | 24 | async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { |
24 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined | 25 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined |
25 | 26 | ||
26 | const video = await loadVideo(id, fetchType, userId) | 27 | const video = await loadVideo(id, fetchType, userId) |
27 | 28 | ||
28 | if (video === null) { | 29 | if (!video) { |
29 | res.fail({ | 30 | res.fail({ |
30 | status: HttpStatusCode.NOT_FOUND_404, | 31 | status: HttpStatusCode.NOT_FOUND_404, |
31 | message: 'Video not found' | 32 | message: 'Video not found' |
32 | }) | 33 | }) |
34 | |||
33 | return false | 35 | return false |
34 | } | 36 | } |
35 | 37 | ||
@@ -58,6 +60,8 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi | |||
58 | return true | 60 | return true |
59 | } | 61 | } |
60 | 62 | ||
63 | // --------------------------------------------------------------------------- | ||
64 | |||
61 | async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) { | 65 | async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) { |
62 | if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) { | 66 | if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) { |
63 | res.fail({ | 67 | res.fail({ |
@@ -70,6 +74,8 @@ async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | st | |||
70 | return true | 74 | return true |
71 | } | 75 | } |
72 | 76 | ||
77 | // --------------------------------------------------------------------------- | ||
78 | |||
73 | async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { | 79 | async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { |
74 | const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) | 80 | const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) |
75 | 81 | ||
@@ -95,32 +101,78 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc | |||
95 | return true | 101 | return true |
96 | } | 102 | } |
97 | 103 | ||
98 | async function checkCanSeeVideoIfPrivate (req: Request, res: Response, video: MVideo, authenticateInQuery = false) { | 104 | // --------------------------------------------------------------------------- |
99 | if (!video.requiresAuth()) return true | ||
100 | 105 | ||
101 | const videoWithRights = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id) | 106 | async function checkCanSeeVideo (options: { |
107 | req: Request | ||
108 | res: Response | ||
109 | paramId: string | ||
110 | video: MVideo | ||
111 | authenticateInQuery?: boolean // default false | ||
112 | }) { | ||
113 | const { req, res, video, paramId, authenticateInQuery = false } = options | ||
114 | |||
115 | if (video.requiresAuth()) { | ||
116 | return checkCanSeeAuthVideo(req, res, video, authenticateInQuery) | ||
117 | } | ||
102 | 118 | ||
103 | return checkCanSeePrivateVideo(req, res, videoWithRights, authenticateInQuery) | 119 | if (video.privacy === VideoPrivacy.UNLISTED) { |
104 | } | 120 | if (isUUIDValid(paramId)) return true |
105 | 121 | ||
106 | async function checkCanSeePrivateVideo (req: Request, res: Response, video: MVideoWithRights, authenticateInQuery = false) { | 122 | return checkCanSeeAuthVideo(req, res, video, authenticateInQuery) |
107 | await authenticatePromiseIfNeeded(req, res, authenticateInQuery) | 123 | } |
124 | |||
125 | if (video.privacy === VideoPrivacy.PUBLIC) return true | ||
108 | 126 | ||
109 | const user = res.locals.oauth ? res.locals.oauth.token.User : null | 127 | throw new Error('Fatal error when checking video right ' + video.url) |
128 | } | ||
110 | 129 | ||
111 | // Only the owner or a user that have blocklist rights can see the video | 130 | async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights, authenticateInQuery = false) { |
112 | if (!user || !user.canGetVideo(video)) { | 131 | const fail = () => { |
113 | res.fail({ | 132 | res.fail({ |
114 | status: HttpStatusCode.FORBIDDEN_403, | 133 | status: HttpStatusCode.FORBIDDEN_403, |
115 | message: 'Cannot fetch information of private/internal/blocklisted video' | 134 | message: 'Cannot fetch information of private/internal/blocked video' |
116 | }) | 135 | }) |
117 | 136 | ||
118 | return false | 137 | return false |
119 | } | 138 | } |
120 | 139 | ||
121 | return true | 140 | await authenticatePromise(req, res, authenticateInQuery) |
141 | |||
142 | const user = res.locals.oauth?.token.User | ||
143 | if (!user) return fail() | ||
144 | |||
145 | const videoWithRights = (video as MVideoWithRights).VideoChannel?.Account?.userId | ||
146 | ? video as MVideoWithRights | ||
147 | : await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id) | ||
148 | |||
149 | const privacy = videoWithRights.privacy | ||
150 | |||
151 | if (privacy === VideoPrivacy.INTERNAL) { | ||
152 | // We know we have a user | ||
153 | return true | ||
154 | } | ||
155 | |||
156 | const isOwnedByUser = videoWithRights.VideoChannel.Account.userId === user.id | ||
157 | |||
158 | if (videoWithRights.isBlacklisted()) { | ||
159 | if (isOwnedByUser || user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) return true | ||
160 | |||
161 | return fail() | ||
162 | } | ||
163 | |||
164 | if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) { | ||
165 | if (isOwnedByUser || user.hasRight(UserRight.SEE_ALL_VIDEOS)) return true | ||
166 | |||
167 | return fail() | ||
168 | } | ||
169 | |||
170 | // Should not happen | ||
171 | return fail() | ||
122 | } | 172 | } |
123 | 173 | ||
174 | // --------------------------------------------------------------------------- | ||
175 | |||
124 | function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { | 176 | function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { |
125 | // Retrieve the user who did the request | 177 | // Retrieve the user who did the request |
126 | if (onlyOwned && video.isOwned() === false) { | 178 | if (onlyOwned && video.isOwned() === false) { |
@@ -146,6 +198,8 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: | |||
146 | return true | 198 | return true |
147 | } | 199 | } |
148 | 200 | ||
201 | // --------------------------------------------------------------------------- | ||
202 | |||
149 | async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) { | 203 | async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) { |
150 | if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { | 204 | if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { |
151 | res.fail({ | 205 | res.fail({ |
@@ -167,7 +221,6 @@ export { | |||
167 | doesVideoFileOfVideoExist, | 221 | doesVideoFileOfVideoExist, |
168 | 222 | ||
169 | checkUserCanManageVideo, | 223 | checkUserCanManageVideo, |
170 | checkCanSeeVideoIfPrivate, | 224 | checkCanSeeVideo, |
171 | checkCanSeePrivateVideo, | ||
172 | checkUserQuota | 225 | checkUserQuota |
173 | } | 226 | } |
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 3ba668460..c9978e3b4 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -28,7 +28,7 @@ function createSortableColumns (sortableColumns: string[]) { | |||
28 | return sortableColumns.concat(sortableColumnDesc) | 28 | return sortableColumns.concat(sortableColumnDesc) |
29 | } | 29 | } |
30 | 30 | ||
31 | const usersSortValidator = checkSortFactory(SORTABLE_COLUMNS.USERS) | 31 | const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS) |
32 | const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS) | 32 | const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS) |
33 | const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ]) | 33 | const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ]) |
34 | const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES) | 34 | const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES) |
@@ -59,7 +59,7 @@ const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CH | |||
59 | // --------------------------------------------------------------------------- | 59 | // --------------------------------------------------------------------------- |
60 | 60 | ||
61 | export { | 61 | export { |
62 | usersSortValidator, | 62 | adminUsersSortValidator, |
63 | abusesSortValidator, | 63 | abusesSortValidator, |
64 | videoChannelsSortValidator, | 64 | videoChannelsSortValidator, |
65 | videoImportsSortValidator, | 65 | videoImportsSortValidator, |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index bc6007c6d..6d306121e 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -486,7 +486,7 @@ const ensureAuthUserOwnsAccountValidator = [ | |||
486 | if (res.locals.account.id !== user.Account.id) { | 486 | if (res.locals.account.id !== user.Account.id) { |
487 | return res.fail({ | 487 | return res.fail({ |
488 | status: HttpStatusCode.FORBIDDEN_403, | 488 | status: HttpStatusCode.FORBIDDEN_403, |
489 | message: 'Only owner of this account can access this ressource.' | 489 | message: 'Only owner of this account can access this resource.' |
490 | }) | 490 | }) |
491 | } | 491 | } |
492 | 492 | ||
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index bd2590bc5..1dd7b5d2e 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts | |||
@@ -9,6 +9,7 @@ export * from './video-ownership-changes' | |||
9 | export * from './video-view' | 9 | export * from './video-view' |
10 | export * from './video-rates' | 10 | export * from './video-rates' |
11 | export * from './video-shares' | 11 | export * from './video-shares' |
12 | export * from './video-source' | ||
12 | export * from './video-stats' | 13 | export * from './video-stats' |
13 | export * from './video-studio' | 14 | export * from './video-studio' |
14 | export * from './video-transcoding' | 15 | export * from './video-transcoding' |
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts index 441c6b4be..dfb8fefc5 100644 --- a/server/middlewares/validators/videos/video-captions.ts +++ b/server/middlewares/validators/videos/video-captions.ts | |||
@@ -7,7 +7,7 @@ import { logger } from '../../../helpers/logger' | |||
7 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants' | 7 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants' |
8 | import { | 8 | import { |
9 | areValidationErrors, | 9 | areValidationErrors, |
10 | checkCanSeeVideoIfPrivate, | 10 | checkCanSeeVideo, |
11 | checkUserCanManageVideo, | 11 | checkUserCanManageVideo, |
12 | doesVideoCaptionExist, | 12 | doesVideoCaptionExist, |
13 | doesVideoExist, | 13 | doesVideoExist, |
@@ -74,7 +74,7 @@ const listVideoCaptionsValidator = [ | |||
74 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return | 74 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return |
75 | 75 | ||
76 | const video = res.locals.onlyVideo | 76 | const video = res.locals.onlyVideo |
77 | if (!await checkCanSeeVideoIfPrivate(req, res, video)) return | 77 | if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.videoId })) return |
78 | 78 | ||
79 | return next() | 79 | return next() |
80 | } | 80 | } |
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 698afdbd1..b22a4e3b7 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts | |||
@@ -10,7 +10,7 @@ import { Hooks } from '../../../lib/plugins/hooks' | |||
10 | import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' | 10 | import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' |
11 | import { | 11 | import { |
12 | areValidationErrors, | 12 | areValidationErrors, |
13 | checkCanSeeVideoIfPrivate, | 13 | checkCanSeeVideo, |
14 | doesVideoCommentExist, | 14 | doesVideoCommentExist, |
15 | doesVideoCommentThreadExist, | 15 | doesVideoCommentThreadExist, |
16 | doesVideoExist, | 16 | doesVideoExist, |
@@ -54,7 +54,7 @@ const listVideoCommentThreadsValidator = [ | |||
54 | if (areValidationErrors(req, res)) return | 54 | if (areValidationErrors(req, res)) return |
55 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return | 55 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return |
56 | 56 | ||
57 | if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return | 57 | if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return |
58 | 58 | ||
59 | return next() | 59 | return next() |
60 | } | 60 | } |
@@ -73,7 +73,7 @@ const listVideoThreadCommentsValidator = [ | |||
73 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return | 73 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return |
74 | if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return | 74 | if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return |
75 | 75 | ||
76 | if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return | 76 | if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return |
77 | 77 | ||
78 | return next() | 78 | return next() |
79 | } | 79 | } |
@@ -91,7 +91,7 @@ const addVideoCommentThreadValidator = [ | |||
91 | if (areValidationErrors(req, res)) return | 91 | if (areValidationErrors(req, res)) return |
92 | if (!await doesVideoExist(req.params.videoId, res)) return | 92 | if (!await doesVideoExist(req.params.videoId, res)) return |
93 | 93 | ||
94 | if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return | 94 | if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return |
95 | 95 | ||
96 | if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return | 96 | if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return |
97 | if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return | 97 | if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return |
@@ -113,7 +113,7 @@ const addVideoCommentReplyValidator = [ | |||
113 | if (areValidationErrors(req, res)) return | 113 | if (areValidationErrors(req, res)) return |
114 | if (!await doesVideoExist(req.params.videoId, res)) return | 114 | if (!await doesVideoExist(req.params.videoId, res)) return |
115 | 115 | ||
116 | if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return | 116 | if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return |
117 | 117 | ||
118 | if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return | 118 | if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return |
119 | if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return | 119 | if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return |
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index 241b9ed7b..d514ae0ad 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts | |||
@@ -33,7 +33,7 @@ import { logger } from '../../../helpers/logger' | |||
33 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 33 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
34 | import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' | 34 | import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' |
35 | import { MVideoPlaylist } from '../../../types/models/video/video-playlist' | 35 | import { MVideoPlaylist } from '../../../types/models/video/video-playlist' |
36 | import { authenticatePromiseIfNeeded } from '../../auth' | 36 | import { authenticatePromise } from '../../auth' |
37 | import { | 37 | import { |
38 | areValidationErrors, | 38 | areValidationErrors, |
39 | doesVideoChannelIdExist, | 39 | doesVideoChannelIdExist, |
@@ -161,7 +161,7 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { | |||
161 | } | 161 | } |
162 | 162 | ||
163 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { | 163 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { |
164 | await authenticatePromiseIfNeeded(req, res) | 164 | await authenticatePromise(req, res) |
165 | 165 | ||
166 | const user = res.locals.oauth ? res.locals.oauth.token.User : null | 166 | const user = res.locals.oauth ? res.locals.oauth.token.User : null |
167 | 167 | ||
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts index 1a9736034..8b8eeedb6 100644 --- a/server/middlewares/validators/videos/video-rates.ts +++ b/server/middlewares/validators/videos/video-rates.ts | |||
@@ -8,7 +8,7 @@ import { isRatingValid } from '../../../helpers/custom-validators/video-rates' | |||
8 | import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' | 8 | import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' |
9 | import { logger } from '../../../helpers/logger' | 9 | import { logger } from '../../../helpers/logger' |
10 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 10 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
11 | import { areValidationErrors, checkCanSeeVideoIfPrivate, doesVideoExist, isValidVideoIdParam } from '../shared' | 11 | import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam } from '../shared' |
12 | 12 | ||
13 | const videoUpdateRateValidator = [ | 13 | const videoUpdateRateValidator = [ |
14 | isValidVideoIdParam('id'), | 14 | isValidVideoIdParam('id'), |
@@ -21,7 +21,7 @@ const videoUpdateRateValidator = [ | |||
21 | if (areValidationErrors(req, res)) return | 21 | if (areValidationErrors(req, res)) return |
22 | if (!await doesVideoExist(req.params.id, res)) return | 22 | if (!await doesVideoExist(req.params.id, res)) return |
23 | 23 | ||
24 | if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return | 24 | if (!await checkCanSeeVideo({ req, res, paramId: req.params.id, video: res.locals.videoAll })) return |
25 | 25 | ||
26 | return next() | 26 | return next() |
27 | } | 27 | } |
diff --git a/server/middlewares/validators/videos/video-source.ts b/server/middlewares/validators/videos/video-source.ts new file mode 100644 index 000000000..31a2f16b3 --- /dev/null +++ b/server/middlewares/validators/videos/video-source.ts | |||
@@ -0,0 +1,37 @@ | |||
1 | import express from 'express' | ||
2 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
3 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
4 | import { MVideoFullLight } from '@server/types/models' | ||
5 | import { HttpStatusCode, UserRight } from '@shared/models' | ||
6 | import { logger } from '../../../helpers/logger' | ||
7 | import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
8 | |||
9 | const videoSourceGetValidator = [ | ||
10 | isValidVideoIdParam('id'), | ||
11 | |||
12 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
13 | logger.debug('Checking videoSourceGet parameters', { parameters: req.params }) | ||
14 | |||
15 | if (areValidationErrors(req, res)) return | ||
16 | if (!await doesVideoExist(req.params.id, res, 'for-api')) return | ||
17 | |||
18 | const video = getVideoWithAttributes(res) as MVideoFullLight | ||
19 | |||
20 | res.locals.videoSource = await VideoSourceModel.loadByVideoId(video.id) | ||
21 | if (!res.locals.videoSource) { | ||
22 | return res.fail({ | ||
23 | status: HttpStatusCode.NOT_FOUND_404, | ||
24 | message: 'Video source not found' | ||
25 | }) | ||
26 | } | ||
27 | |||
28 | const user = res.locals.oauth.token.User | ||
29 | if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return | ||
30 | |||
31 | return next() | ||
32 | } | ||
33 | ] | ||
34 | |||
35 | export { | ||
36 | videoSourceGetValidator | ||
37 | } | ||
diff --git a/server/middlewares/validators/videos/video-view.ts b/server/middlewares/validators/videos/video-view.ts index 7a4994e8a..2edcd140f 100644 --- a/server/middlewares/validators/videos/video-view.ts +++ b/server/middlewares/validators/videos/video-view.ts | |||
@@ -6,6 +6,7 @@ import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | |||
6 | import { exists, isIdValid, isIntOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | 6 | import { exists, isIdValid, isIntOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' |
7 | import { logger } from '../../../helpers/logger' | 7 | import { logger } from '../../../helpers/logger' |
8 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | 8 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' |
9 | import { getCachedVideoDuration } from '@server/lib/video' | ||
9 | 10 | ||
10 | const getVideoLocalViewerValidator = [ | 11 | const getVideoLocalViewerValidator = [ |
11 | param('localViewerId') | 12 | param('localViewerId') |
@@ -42,20 +43,18 @@ const videoViewValidator = [ | |||
42 | logger.debug('Checking videoView parameters', { parameters: req.body }) | 43 | logger.debug('Checking videoView parameters', { parameters: req.body }) |
43 | 44 | ||
44 | if (areValidationErrors(req, res)) return | 45 | if (areValidationErrors(req, res)) return |
45 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return | 46 | if (!await doesVideoExist(req.params.videoId, res, 'only-immutable-attributes')) return |
46 | 47 | ||
47 | const video = res.locals.onlyVideo | 48 | const video = res.locals.onlyImmutableVideo |
48 | const videoDuration = video.isLive | 49 | const { duration } = await getCachedVideoDuration(video.id) |
49 | ? undefined | ||
50 | : video.duration | ||
51 | 50 | ||
52 | if (!exists(req.body.currentTime)) { // TODO: remove in a few versions, introduced in 4.2 | 51 | if (!exists(req.body.currentTime)) { // TODO: remove in a few versions, introduced in 4.2 |
53 | req.body.currentTime = Math.min(videoDuration ?? 0, 30) | 52 | req.body.currentTime = Math.min(duration ?? 0, 30) |
54 | } | 53 | } |
55 | 54 | ||
56 | const currentTime: number = req.body.currentTime | 55 | const currentTime: number = req.body.currentTime |
57 | 56 | ||
58 | if (!isVideoTimeValid(currentTime, videoDuration)) { | 57 | if (!isVideoTimeValid(currentTime, duration)) { |
59 | return res.fail({ | 58 | return res.fail({ |
60 | status: HttpStatusCode.BAD_REQUEST_400, | 59 | status: HttpStatusCode.BAD_REQUEST_400, |
61 | message: 'Current time is invalid' | 60 | message: 'Current time is invalid' |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 0b6b8bfe5..c6d31f8f0 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -7,14 +7,13 @@ import { getServerActor } from '@server/models/application/application' | |||
7 | import { ExpressPromiseHandler } from '@server/types/express-handler' | 7 | import { ExpressPromiseHandler } from '@server/types/express-handler' |
8 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' | 8 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' |
9 | import { getAllPrivacies } from '@shared/core-utils' | 9 | import { getAllPrivacies } from '@shared/core-utils' |
10 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude, VideoPrivacy } from '@shared/models' | 10 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude } from '@shared/models' |
11 | import { | 11 | import { |
12 | exists, | 12 | exists, |
13 | isBooleanValid, | 13 | isBooleanValid, |
14 | isDateValid, | 14 | isDateValid, |
15 | isFileValid, | 15 | isFileValid, |
16 | isIdValid, | 16 | isIdValid, |
17 | isUUIDValid, | ||
18 | toArray, | 17 | toArray, |
19 | toBooleanOrNull, | 18 | toBooleanOrNull, |
20 | toIntOrNull, | 19 | toIntOrNull, |
@@ -50,7 +49,7 @@ import { Hooks } from '../../../lib/plugins/hooks' | |||
50 | import { VideoModel } from '../../../models/video/video' | 49 | import { VideoModel } from '../../../models/video/video' |
51 | import { | 50 | import { |
52 | areValidationErrors, | 51 | areValidationErrors, |
53 | checkCanSeePrivateVideo, | 52 | checkCanSeeVideo, |
54 | checkUserCanManageVideo, | 53 | checkUserCanManageVideo, |
55 | checkUserQuota, | 54 | checkUserQuota, |
56 | doesVideoChannelOfAccountExist, | 55 | doesVideoChannelOfAccountExist, |
@@ -152,7 +151,7 @@ const videosAddResumableValidator = [ | |||
152 | 151 | ||
153 | if (!await isVideoAccepted(req, res, file)) return cleanup() | 152 | if (!await isVideoAccepted(req, res, file)) return cleanup() |
154 | 153 | ||
155 | res.locals.videoFileResumable = file | 154 | res.locals.videoFileResumable = { ...file, originalname: file.filename } |
156 | 155 | ||
157 | return next() | 156 | return next() |
158 | } | 157 | } |
@@ -297,28 +296,9 @@ const videosCustomGetValidator = ( | |||
297 | 296 | ||
298 | const video = getVideoWithAttributes(res) as MVideoFullLight | 297 | const video = getVideoWithAttributes(res) as MVideoFullLight |
299 | 298 | ||
300 | // Video private or blacklisted | 299 | if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id, authenticateInQuery })) return |
301 | if (video.requiresAuth()) { | ||
302 | if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) { | ||
303 | return next() | ||
304 | } | ||
305 | 300 | ||
306 | return | 301 | return next() |
307 | } | ||
308 | |||
309 | // Video is public, anyone can access it | ||
310 | if (video.privacy === VideoPrivacy.PUBLIC) return next() | ||
311 | |||
312 | // Video is unlisted, check we used the uuid to fetch it | ||
313 | if (video.privacy === VideoPrivacy.UNLISTED) { | ||
314 | if (isUUIDValid(req.params.id)) return next() | ||
315 | |||
316 | // Don't leak this unlisted video | ||
317 | return res.fail({ | ||
318 | status: HttpStatusCode.NOT_FOUND_404, | ||
319 | message: 'Video not found' | ||
320 | }) | ||
321 | } | ||
322 | } | 302 | } |
323 | ] | 303 | ] |
324 | } | 304 | } |
diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/abuse-query-builder.ts index 025e6ba55..cfc924ba4 100644 --- a/server/models/abuse/abuse-query-builder.ts +++ b/server/models/abuse/abuse-query-builder.ts | |||
@@ -13,7 +13,7 @@ export type BuildAbusesQueryOptions = { | |||
13 | searchReporter?: string | 13 | searchReporter?: string |
14 | searchReportee?: string | 14 | searchReportee?: string |
15 | 15 | ||
16 | // video releated | 16 | // video related |
17 | searchVideo?: string | 17 | searchVideo?: string |
18 | searchVideoChannel?: string | 18 | searchVideoChannel?: string |
19 | videoIs?: AbuseVideoIs | 19 | videoIs?: AbuseVideoIs |
diff --git a/server/models/shared/abstract-run-query.ts b/server/models/shared/abstract-run-query.ts index f1182c7be..7f27a0c4b 100644 --- a/server/models/shared/abstract-run-query.ts +++ b/server/models/shared/abstract-run-query.ts | |||
@@ -2,7 +2,7 @@ import { QueryTypes, Sequelize, Transaction } from 'sequelize' | |||
2 | 2 | ||
3 | /** | 3 | /** |
4 | * | 4 | * |
5 | * Abstact builder to run video SQL queries | 5 | * Abstract builder to run video SQL queries |
6 | * | 6 | * |
7 | */ | 7 | */ |
8 | 8 | ||
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 68b2bf523..dc260e512 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -29,12 +29,11 @@ import { | |||
29 | MUserDefault, | 29 | MUserDefault, |
30 | MUserFormattable, | 30 | MUserFormattable, |
31 | MUserNotifSettingChannelDefault, | 31 | MUserNotifSettingChannelDefault, |
32 | MUserWithNotificationSetting, | 32 | MUserWithNotificationSetting |
33 | MVideoWithRights | ||
34 | } from '@server/types/models' | 33 | } from '@server/types/models' |
35 | import { AttributesOnly } from '@shared/typescript-utils' | 34 | import { AttributesOnly } from '@shared/typescript-utils' |
36 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' | 35 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' |
37 | import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' | 36 | import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models' |
38 | import { User, UserRole } from '../../../shared/models/users' | 37 | import { User, UserRole } from '../../../shared/models/users' |
39 | import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' | 38 | import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' |
40 | import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' | 39 | import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' |
@@ -66,7 +65,7 @@ import { ActorModel } from '../actor/actor' | |||
66 | import { ActorFollowModel } from '../actor/actor-follow' | 65 | import { ActorFollowModel } from '../actor/actor-follow' |
67 | import { ActorImageModel } from '../actor/actor-image' | 66 | import { ActorImageModel } from '../actor/actor-image' |
68 | import { OAuthTokenModel } from '../oauth/oauth-token' | 67 | import { OAuthTokenModel } from '../oauth/oauth-token' |
69 | import { getSort, throwIfNotValid } from '../utils' | 68 | import { getAdminUsersSort, throwIfNotValid } from '../utils' |
70 | import { VideoModel } from '../video/video' | 69 | import { VideoModel } from '../video/video' |
71 | import { VideoChannelModel } from '../video/video-channel' | 70 | import { VideoChannelModel } from '../video/video-channel' |
72 | import { VideoImportModel } from '../video/video-import' | 71 | import { VideoImportModel } from '../video/video-import' |
@@ -461,7 +460,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
461 | return this.count() | 460 | return this.count() |
462 | } | 461 | } |
463 | 462 | ||
464 | static listForApi (parameters: { | 463 | static listForAdminApi (parameters: { |
465 | start: number | 464 | start: number |
466 | count: number | 465 | count: number |
467 | sort: string | 466 | sort: string |
@@ -497,7 +496,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
497 | const query: FindOptions = { | 496 | const query: FindOptions = { |
498 | offset: start, | 497 | offset: start, |
499 | limit: count, | 498 | limit: count, |
500 | order: getSort(sort), | 499 | order: getAdminUsersSort(sort), |
501 | where | 500 | where |
502 | } | 501 | } |
503 | 502 | ||
@@ -851,22 +850,6 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
851 | .then(u => u.map(u => u.username)) | 850 | .then(u => u.map(u => u.username)) |
852 | } | 851 | } |
853 | 852 | ||
854 | canGetVideo (video: MVideoWithRights) { | ||
855 | const videoUserId = video.VideoChannel.Account.userId | ||
856 | |||
857 | if (video.isBlacklisted()) { | ||
858 | return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) | ||
859 | } | ||
860 | |||
861 | if (video.privacy === VideoPrivacy.PRIVATE) { | ||
862 | return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) | ||
863 | } | ||
864 | |||
865 | if (video.privacy === VideoPrivacy.INTERNAL) return true | ||
866 | |||
867 | return false | ||
868 | } | ||
869 | |||
870 | hasRight (right: UserRight) { | 853 | hasRight (right: UserRight) { |
871 | return hasUserRight(this.role, right) | 854 | return hasUserRight(this.role, right) |
872 | } | 855 | } |
diff --git a/server/models/utils.ts b/server/models/utils.ts index b57290aff..c468f748d 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -11,8 +11,6 @@ function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderIt | |||
11 | 11 | ||
12 | if (field.toLowerCase() === 'match') { // Search | 12 | if (field.toLowerCase() === 'match') { // Search |
13 | finalField = Sequelize.col('similarity') | 13 | finalField = Sequelize.col('similarity') |
14 | } else if (field === 'videoQuotaUsed') { // Users list | ||
15 | finalField = Sequelize.col('videoQuotaUsed') | ||
16 | } else { | 14 | } else { |
17 | finalField = field | 15 | finalField = field |
18 | } | 16 | } |
@@ -20,6 +18,25 @@ function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderIt | |||
20 | return [ [ finalField, direction ], lastSort ] | 18 | return [ [ finalField, direction ], lastSort ] |
21 | } | 19 | } |
22 | 20 | ||
21 | function getAdminUsersSort (value: string): OrderItem[] { | ||
22 | const { direction, field } = buildDirectionAndField(value) | ||
23 | |||
24 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
25 | |||
26 | if (field === 'videoQuotaUsed') { // Users list | ||
27 | finalField = Sequelize.col('videoQuotaUsed') | ||
28 | } else { | ||
29 | finalField = field | ||
30 | } | ||
31 | |||
32 | const nullPolicy = direction === 'ASC' | ||
33 | ? 'NULLS FIRST' | ||
34 | : 'NULLS LAST' | ||
35 | |||
36 | // FIXME: typings | ||
37 | return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ] | ||
38 | } | ||
39 | |||
23 | function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | 40 | function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { |
24 | const { direction, field } = buildDirectionAndField(value) | 41 | const { direction, field } = buildDirectionAndField(value) |
25 | 42 | ||
@@ -102,7 +119,7 @@ function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'A | |||
102 | 119 | ||
103 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { | 120 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { |
104 | if (!model.createdAt || !model.updatedAt) { | 121 | if (!model.createdAt || !model.updatedAt) { |
105 | throw new Error('Miss createdAt & updatedAt attribuets to model') | 122 | throw new Error('Miss createdAt & updatedAt attributes to model') |
106 | } | 123 | } |
107 | 124 | ||
108 | const now = Date.now() | 125 | const now = Date.now() |
@@ -260,6 +277,7 @@ export { | |||
260 | buildLocalAccountIdsIn, | 277 | buildLocalAccountIdsIn, |
261 | getSort, | 278 | getSort, |
262 | getCommentSort, | 279 | getCommentSort, |
280 | getAdminUsersSort, | ||
263 | getVideoSort, | 281 | getVideoSort, |
264 | getBlacklistSort, | 282 | getBlacklistSort, |
265 | createSimilarityAttribute, | 283 | createSimilarityAttribute, |
diff --git a/server/models/video/sql/video/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index b79d20ade..3c74b0ea6 100644 --- a/server/models/video/sql/video/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts | |||
@@ -313,7 +313,12 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
313 | return result | 313 | return result |
314 | } | 314 | } |
315 | 315 | ||
316 | protected whereId (options: { id?: string | number, url?: string }) { | 316 | protected whereId (options: { ids?: number[], id?: string | number, url?: string }) { |
317 | if (options.ids) { | ||
318 | this.where = `WHERE "video"."id" IN (${createSafeIn(this.sequelize, options.ids)})` | ||
319 | return | ||
320 | } | ||
321 | |||
317 | if (options.url) { | 322 | if (options.url) { |
318 | this.where = 'WHERE "video"."url" = :videoUrl' | 323 | this.where = 'WHERE "video"."url" = :videoUrl' |
319 | this.replacements.videoUrl = options.url | 324 | this.replacements.videoUrl = options.url |
diff --git a/server/models/video/sql/video/shared/video-file-query-builder.ts b/server/models/video/sql/video/shared/video-file-query-builder.ts index 50c12f627..cc53a4860 100644 --- a/server/models/video/sql/video/shared/video-file-query-builder.ts +++ b/server/models/video/sql/video/shared/video-file-query-builder.ts | |||
@@ -1,7 +1,17 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize, Transaction } from 'sequelize' |
2 | import { BuildVideoGetQueryOptions } from '../video-model-get-query-builder' | ||
3 | import { AbstractVideoQueryBuilder } from './abstract-video-query-builder' | 2 | import { AbstractVideoQueryBuilder } from './abstract-video-query-builder' |
4 | 3 | ||
4 | export type FileQueryOptions = { | ||
5 | id?: string | number | ||
6 | url?: string | ||
7 | |||
8 | includeRedundancy: boolean | ||
9 | |||
10 | transaction?: Transaction | ||
11 | |||
12 | logging?: boolean | ||
13 | } | ||
14 | |||
5 | /** | 15 | /** |
6 | * | 16 | * |
7 | * Fetch files (webtorrent and streaming playlist) according to a video | 17 | * Fetch files (webtorrent and streaming playlist) according to a video |
@@ -15,26 +25,26 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
15 | super(sequelize, 'get') | 25 | super(sequelize, 'get') |
16 | } | 26 | } |
17 | 27 | ||
18 | queryWebTorrentVideos (options: BuildVideoGetQueryOptions) { | 28 | queryWebTorrentVideos (options: FileQueryOptions) { |
19 | this.buildWebtorrentFilesQuery(options) | 29 | this.buildWebtorrentFilesQuery(options) |
20 | 30 | ||
21 | return this.runQuery(options) | 31 | return this.runQuery(options) |
22 | } | 32 | } |
23 | 33 | ||
24 | queryStreamingPlaylistVideos (options: BuildVideoGetQueryOptions) { | 34 | queryStreamingPlaylistVideos (options: FileQueryOptions) { |
25 | this.buildVideoStreamingPlaylistFilesQuery(options) | 35 | this.buildVideoStreamingPlaylistFilesQuery(options) |
26 | 36 | ||
27 | return this.runQuery(options) | 37 | return this.runQuery(options) |
28 | } | 38 | } |
29 | 39 | ||
30 | private buildWebtorrentFilesQuery (options: BuildVideoGetQueryOptions) { | 40 | private buildWebtorrentFilesQuery (options: FileQueryOptions) { |
31 | this.attributes = { | 41 | this.attributes = { |
32 | '"video"."id"': '' | 42 | '"video"."id"': '' |
33 | } | 43 | } |
34 | 44 | ||
35 | this.includeWebtorrentFiles() | 45 | this.includeWebtorrentFiles() |
36 | 46 | ||
37 | if (this.shouldIncludeRedundancies(options)) { | 47 | if (options.includeRedundancy) { |
38 | this.includeWebTorrentRedundancies() | 48 | this.includeWebTorrentRedundancies() |
39 | } | 49 | } |
40 | 50 | ||
@@ -43,14 +53,14 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
43 | this.query = this.buildQuery() | 53 | this.query = this.buildQuery() |
44 | } | 54 | } |
45 | 55 | ||
46 | private buildVideoStreamingPlaylistFilesQuery (options: BuildVideoGetQueryOptions) { | 56 | private buildVideoStreamingPlaylistFilesQuery (options: FileQueryOptions) { |
47 | this.attributes = { | 57 | this.attributes = { |
48 | '"video"."id"': '' | 58 | '"video"."id"': '' |
49 | } | 59 | } |
50 | 60 | ||
51 | this.includeStreamingPlaylistFiles() | 61 | this.includeStreamingPlaylistFiles() |
52 | 62 | ||
53 | if (this.shouldIncludeRedundancies(options)) { | 63 | if (options.includeRedundancy) { |
54 | this.includeStreamingPlaylistRedundancies() | 64 | this.includeStreamingPlaylistRedundancies() |
55 | } | 65 | } |
56 | 66 | ||
@@ -62,8 +72,4 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
62 | private buildQuery () { | 72 | private buildQuery () { |
63 | return `${this.buildSelect()} FROM "video" ${this.joins} ${this.where}` | 73 | return `${this.buildSelect()} FROM "video" ${this.joins} ${this.where}` |
64 | } | 74 | } |
65 | |||
66 | private shouldIncludeRedundancies (options: BuildVideoGetQueryOptions) { | ||
67 | return options.type === 'api' | ||
68 | } | ||
69 | } | 75 | } |
diff --git a/server/models/video/sql/video/video-model-get-query-builder.ts b/server/models/video/sql/video/video-model-get-query-builder.ts index b0879c9ac..32e5c4ff7 100644 --- a/server/models/video/sql/video/video-model-get-query-builder.ts +++ b/server/models/video/sql/video/video-model-get-query-builder.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { pick } from 'lodash' | ||
1 | import { Sequelize, Transaction } from 'sequelize' | 2 | import { Sequelize, Transaction } from 'sequelize' |
2 | import { AbstractVideoQueryBuilder } from './shared/abstract-video-query-builder' | 3 | import { AbstractVideoQueryBuilder } from './shared/abstract-video-query-builder' |
3 | import { VideoFileQueryBuilder } from './shared/video-file-query-builder' | 4 | import { VideoFileQueryBuilder } from './shared/video-file-query-builder' |
@@ -50,15 +51,21 @@ export class VideoModelGetQueryBuilder { | |||
50 | } | 51 | } |
51 | 52 | ||
52 | async queryVideo (options: BuildVideoGetQueryOptions) { | 53 | async queryVideo (options: BuildVideoGetQueryOptions) { |
54 | const fileQueryOptions = { | ||
55 | ...pick(options, [ 'id', 'url', 'transaction', 'logging' ]), | ||
56 | |||
57 | includeRedundancy: this.shouldIncludeRedundancies(options) | ||
58 | } | ||
59 | |||
53 | const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ | 60 | const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ |
54 | this.videoQueryBuilder.queryVideos(options), | 61 | this.videoQueryBuilder.queryVideos(options), |
55 | 62 | ||
56 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) | 63 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) |
57 | ? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(options) | 64 | ? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(fileQueryOptions) |
58 | : Promise.resolve(undefined), | 65 | : Promise.resolve(undefined), |
59 | 66 | ||
60 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) | 67 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) |
61 | ? this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(options) | 68 | ? this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(fileQueryOptions) |
62 | : Promise.resolve(undefined) | 69 | : Promise.resolve(undefined) |
63 | ]) | 70 | ]) |
64 | 71 | ||
@@ -76,6 +83,10 @@ export class VideoModelGetQueryBuilder { | |||
76 | 83 | ||
77 | return videos[0] | 84 | return videos[0] |
78 | } | 85 | } |
86 | |||
87 | private shouldIncludeRedundancies (options: BuildVideoGetQueryOptions) { | ||
88 | return options.type === 'api' | ||
89 | } | ||
79 | } | 90 | } |
80 | 91 | ||
81 | export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder { | 92 | export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder { |
diff --git a/server/models/video/sql/video/videos-model-list-query-builder.ts b/server/models/video/sql/video/videos-model-list-query-builder.ts index 2a4afc389..4fe6bc321 100644 --- a/server/models/video/sql/video/videos-model-list-query-builder.ts +++ b/server/models/video/sql/video/videos-model-list-query-builder.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import { VideoInclude } from '@shared/models' | 1 | import { pick } from 'lodash' |
2 | import { Sequelize } from 'sequelize' | 2 | import { Sequelize } from 'sequelize' |
3 | import { VideoInclude } from '@shared/models' | ||
3 | import { AbstractVideoQueryBuilder } from './shared/abstract-video-query-builder' | 4 | import { AbstractVideoQueryBuilder } from './shared/abstract-video-query-builder' |
5 | import { VideoFileQueryBuilder } from './shared/video-file-query-builder' | ||
4 | import { VideoModelBuilder } from './shared/video-model-builder' | 6 | import { VideoModelBuilder } from './shared/video-model-builder' |
5 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder' | 7 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder' |
6 | 8 | ||
@@ -16,20 +18,46 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
16 | private innerQuery: string | 18 | private innerQuery: string |
17 | private innerSort: string | 19 | private innerSort: string |
18 | 20 | ||
21 | webtorrentFilesQueryBuilder: VideoFileQueryBuilder | ||
22 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder | ||
23 | |||
19 | private readonly videoModelBuilder: VideoModelBuilder | 24 | private readonly videoModelBuilder: VideoModelBuilder |
20 | 25 | ||
21 | constructor (protected readonly sequelize: Sequelize) { | 26 | constructor (protected readonly sequelize: Sequelize) { |
22 | super(sequelize, 'list') | 27 | super(sequelize, 'list') |
23 | 28 | ||
24 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) | 29 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) |
30 | this.webtorrentFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | ||
31 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | ||
25 | } | 32 | } |
26 | 33 | ||
27 | queryVideos (options: BuildVideosListQueryOptions) { | 34 | async queryVideos (options: BuildVideosListQueryOptions) { |
28 | this.buildInnerQuery(options) | 35 | this.buildInnerQuery(options) |
29 | this.buildMainQuery(options) | 36 | this.buildMainQuery(options) |
30 | 37 | ||
31 | return this.runQuery() | 38 | const rows = await this.runQuery() |
32 | .then(rows => this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include })) | 39 | |
40 | if (options.include & VideoInclude.FILES) { | ||
41 | const videoIds = Array.from(new Set(rows.map(r => r.id))) | ||
42 | |||
43 | if (videoIds.length !== 0) { | ||
44 | const fileQueryOptions = { | ||
45 | ...pick(options, [ 'transaction', 'logging' ]), | ||
46 | |||
47 | ids: videoIds, | ||
48 | includeRedundancy: false | ||
49 | } | ||
50 | |||
51 | const [ rowsWebTorrentFiles, rowsStreamingPlaylist ] = await Promise.all([ | ||
52 | this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(fileQueryOptions), | ||
53 | this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(fileQueryOptions) | ||
54 | ]) | ||
55 | |||
56 | return this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include, rowsStreamingPlaylist, rowsWebTorrentFiles }) | ||
57 | } | ||
58 | } | ||
59 | |||
60 | return this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include }) | ||
33 | } | 61 | } |
34 | 62 | ||
35 | private buildInnerQuery (options: BuildVideosListQueryOptions) { | 63 | private buildInnerQuery (options: BuildVideosListQueryOptions) { |
@@ -52,11 +80,6 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
52 | this.includeAccounts() | 80 | this.includeAccounts() |
53 | this.includeThumbnails() | 81 | this.includeThumbnails() |
54 | 82 | ||
55 | if (options.include & VideoInclude.FILES) { | ||
56 | this.includeWebtorrentFiles() | ||
57 | this.includeStreamingPlaylistFiles() | ||
58 | } | ||
59 | |||
60 | if (options.user) { | 83 | if (options.user) { |
61 | this.includeUserHistory(options.user.id) | 84 | this.includeUserHistory(options.user.id) |
62 | } | 85 | } |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index d6dd1b8bb..91dafbcf1 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -311,6 +311,16 @@ export type SummaryOptions = { | |||
311 | ')' | 311 | ')' |
312 | ), | 312 | ), |
313 | 'viewsPerDay' | 313 | 'viewsPerDay' |
314 | ], | ||
315 | [ | ||
316 | literal( | ||
317 | '(' + | ||
318 | 'SELECT COALESCE(SUM("video".views), 0) AS totalViews ' + | ||
319 | 'FROM "video" ' + | ||
320 | 'WHERE "video"."channelId" = "VideoChannelModel"."id"' + | ||
321 | ')' | ||
322 | ), | ||
323 | 'totalViews' | ||
314 | ] | 324 | ] |
315 | ] | 325 | ] |
316 | } | 326 | } |
@@ -766,6 +776,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
766 | }) | 776 | }) |
767 | } | 777 | } |
768 | 778 | ||
779 | const totalViews = this.get('totalViews') as number | ||
780 | |||
769 | const actor = this.Actor.toFormattedJSON() | 781 | const actor = this.Actor.toFormattedJSON() |
770 | const videoChannel = { | 782 | const videoChannel = { |
771 | id: this.id, | 783 | id: this.id, |
@@ -779,6 +791,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
779 | 791 | ||
780 | videosCount, | 792 | videosCount, |
781 | viewsPerDay, | 793 | viewsPerDay, |
794 | totalViews, | ||
782 | 795 | ||
783 | avatars: actor.avatars, | 796 | avatars: actor.avatars, |
784 | 797 | ||
diff --git a/server/models/video/video-live-session.ts b/server/models/video/video-live-session.ts index 2b4cde9f8..836620872 100644 --- a/server/models/video/video-live-session.ts +++ b/server/models/video/video-live-session.ts | |||
@@ -110,7 +110,7 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv | |||
110 | static listSessionsOfLiveForAPI (options: { videoId: number }) { | 110 | static listSessionsOfLiveForAPI (options: { videoId: number }) { |
111 | const { videoId } = options | 111 | const { videoId } = options |
112 | 112 | ||
113 | const query: FindOptions<VideoLiveSessionModel> = { | 113 | const query: FindOptions<AttributesOnly<VideoLiveSessionModel>> = { |
114 | where: { | 114 | where: { |
115 | liveVideoId: videoId | 115 | liveVideoId: videoId |
116 | }, | 116 | }, |
diff --git a/server/models/video/video-source.ts b/server/models/video/video-source.ts new file mode 100644 index 000000000..e306b160d --- /dev/null +++ b/server/models/video/video-source.ts | |||
@@ -0,0 +1,55 @@ | |||
1 | import { Op } from 'sequelize' | ||
2 | import { | ||
3 | AllowNull, | ||
4 | BelongsTo, | ||
5 | Column, | ||
6 | CreatedAt, | ||
7 | ForeignKey, | ||
8 | Model, | ||
9 | Table, | ||
10 | UpdatedAt | ||
11 | } from 'sequelize-typescript' | ||
12 | import { AttributesOnly } from '@shared/typescript-utils' | ||
13 | import { VideoModel } from './video' | ||
14 | |||
15 | @Table({ | ||
16 | tableName: 'videoSource', | ||
17 | indexes: [ | ||
18 | { | ||
19 | fields: [ 'videoId' ], | ||
20 | where: { | ||
21 | videoId: { | ||
22 | [Op.ne]: null | ||
23 | } | ||
24 | } | ||
25 | } | ||
26 | ] | ||
27 | }) | ||
28 | export class VideoSourceModel extends Model<Partial<AttributesOnly<VideoSourceModel>>> { | ||
29 | @CreatedAt | ||
30 | createdAt: Date | ||
31 | |||
32 | @UpdatedAt | ||
33 | updatedAt: Date | ||
34 | |||
35 | @AllowNull(false) | ||
36 | @Column | ||
37 | filename: string | ||
38 | |||
39 | @ForeignKey(() => VideoModel) | ||
40 | @Column | ||
41 | videoId: number | ||
42 | |||
43 | @BelongsTo(() => VideoModel) | ||
44 | Video: VideoModel | ||
45 | |||
46 | static loadByVideoId (videoId) { | ||
47 | return VideoSourceModel.findOne({ where: { videoId } }) | ||
48 | } | ||
49 | |||
50 | toFormattedJSON () { | ||
51 | return { | ||
52 | filename: this.filename | ||
53 | } | ||
54 | } | ||
55 | } | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e6a8d3f95..08adbced6 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -136,6 +136,7 @@ import { VideoPlaylistElementModel } from './video-playlist-element' | |||
136 | import { VideoShareModel } from './video-share' | 136 | import { VideoShareModel } from './video-share' |
137 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 137 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
138 | import { VideoTagModel } from './video-tag' | 138 | import { VideoTagModel } from './video-tag' |
139 | import { VideoSourceModel } from './video-source' | ||
139 | 140 | ||
140 | export enum ScopeNames { | 141 | export enum ScopeNames { |
141 | FOR_API = 'FOR_API', | 142 | FOR_API = 'FOR_API', |
@@ -597,6 +598,15 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
597 | }) | 598 | }) |
598 | VideoPlaylistElements: VideoPlaylistElementModel[] | 599 | VideoPlaylistElements: VideoPlaylistElementModel[] |
599 | 600 | ||
601 | @HasOne(() => VideoSourceModel, { | ||
602 | foreignKey: { | ||
603 | name: 'videoId', | ||
604 | allowNull: true | ||
605 | }, | ||
606 | onDelete: 'CASCADE' | ||
607 | }) | ||
608 | VideoSource: VideoSourceModel | ||
609 | |||
600 | @HasMany(() => VideoAbuseModel, { | 610 | @HasMany(() => VideoAbuseModel, { |
601 | foreignKey: { | 611 | foreignKey: { |
602 | name: 'videoId', | 612 | name: 'videoId', |
diff --git a/server/tests/api/check-params/abuses.ts b/server/tests/api/check-params/abuses.ts index c4b051723..bc2cc640f 100644 --- a/server/tests/api/check-params/abuses.ts +++ b/server/tests/api/check-params/abuses.ts | |||
@@ -269,7 +269,7 @@ describe('Test abuses API validators', function () { | |||
269 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) | 269 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) |
270 | }) | 270 | }) |
271 | 271 | ||
272 | it('Should succeed with the corret parameters (advanced)', async function () { | 272 | it('Should succeed with the correct parameters (advanced)', async function () { |
273 | const fields: AbuseCreate = { | 273 | const fields: AbuseCreate = { |
274 | video: { | 274 | video: { |
275 | id: server.store.videoCreated.id, | 275 | id: server.store.videoCreated.id, |
@@ -333,7 +333,7 @@ describe('Test abuses API validators', function () { | |||
333 | await command.addMessage({ token: userToken, abuseId, message: 'a'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 333 | await command.addMessage({ token: userToken, abuseId, message: 'a'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
334 | }) | 334 | }) |
335 | 335 | ||
336 | it('Should suceed with the correct params', async function () { | 336 | it('Should succeed with the correct params', async function () { |
337 | const res = await command.addMessage({ token: userToken, abuseId, message }) | 337 | const res = await command.addMessage({ token: userToken, abuseId, message }) |
338 | messageId = res.body.abuseMessage.id | 338 | messageId = res.body.abuseMessage.id |
339 | }) | 339 | }) |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 259d7e783..a27bc8509 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -3,14 +3,14 @@ import './accounts' | |||
3 | import './blocklist' | 3 | import './blocklist' |
4 | import './bulk' | 4 | import './bulk' |
5 | import './config' | 5 | import './config' |
6 | import './custom-pages' | ||
7 | import './contact-form' | 6 | import './contact-form' |
7 | import './custom-pages' | ||
8 | import './debug' | 8 | import './debug' |
9 | import './follows' | 9 | import './follows' |
10 | import './jobs' | 10 | import './jobs' |
11 | import './live' | ||
11 | import './logs' | 12 | import './logs' |
12 | import './my-user' | 13 | import './my-user' |
13 | import './live' | ||
14 | import './plugins' | 14 | import './plugins' |
15 | import './redundancy' | 15 | import './redundancy' |
16 | import './search' | 16 | import './search' |
@@ -25,12 +25,13 @@ import './video-blacklist' | |||
25 | import './video-captions' | 25 | import './video-captions' |
26 | import './video-channels' | 26 | import './video-channels' |
27 | import './video-comments' | 27 | import './video-comments' |
28 | import './video-studio' | 28 | import './video-files' |
29 | import './video-imports' | 29 | import './video-imports' |
30 | import './video-playlists' | 30 | import './video-playlists' |
31 | import './videos' | 31 | import './video-source' |
32 | import './video-studio' | ||
32 | import './videos-common-filters' | 33 | import './videos-common-filters' |
33 | import './video-files' | ||
34 | import './videos-history' | 34 | import './videos-history' |
35 | import './videos-overviews' | 35 | import './videos-overviews' |
36 | import './videos' | ||
36 | import './views' | 37 | import './views' |
diff --git a/server/tests/api/check-params/video-source.ts b/server/tests/api/check-params/video-source.ts new file mode 100644 index 000000000..ca324bb9d --- /dev/null +++ b/server/tests/api/check-params/video-source.ts | |||
@@ -0,0 +1,44 @@ | |||
1 | import { HttpStatusCode } from '@shared/models' | ||
2 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
3 | |||
4 | describe('Test video sources API validator', function () { | ||
5 | let server: PeerTubeServer = null | ||
6 | let uuid: string | ||
7 | let userToken: string | ||
8 | |||
9 | before(async function () { | ||
10 | this.timeout(30000) | ||
11 | |||
12 | server = await createSingleServer(1) | ||
13 | await setAccessTokensToServers([ server ]) | ||
14 | |||
15 | const created = await server.videos.quickUpload({ name: 'video' }) | ||
16 | uuid = created.uuid | ||
17 | |||
18 | userToken = await server.users.generateUserAndToken('user') | ||
19 | }) | ||
20 | |||
21 | it('Should fail without a valid uuid', async function () { | ||
22 | await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
23 | }) | ||
24 | |||
25 | it('Should receive 404 when passing a non existing video id', async function () { | ||
26 | await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
27 | }) | ||
28 | |||
29 | it('Should not get the source as unauthenticated', async function () { | ||
30 | await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null }) | ||
31 | }) | ||
32 | |||
33 | it('Should not get the source with another user', async function () { | ||
34 | await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: userToken }) | ||
35 | }) | ||
36 | |||
37 | it('Should succeed with the correct parameters get the source as another user', async function () { | ||
38 | await server.videos.getSource({ id: uuid }) | ||
39 | }) | ||
40 | |||
41 | after(async function () { | ||
42 | await cleanupTests([ server ]) | ||
43 | }) | ||
44 | }) | ||
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index 41064d2ff..5ff51d1ff 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts | |||
@@ -39,10 +39,7 @@ describe('Test videos API validator', function () { | |||
39 | 39 | ||
40 | await setAccessTokensToServers([ server ]) | 40 | await setAccessTokensToServers([ server ]) |
41 | 41 | ||
42 | const username = 'user1' | 42 | userAccessToken = await server.users.generateUserAndToken('user1') |
43 | const password = 'my super password' | ||
44 | await server.users.create({ username: username, password: password }) | ||
45 | userAccessToken = await server.login.getAccessToken({ username, password }) | ||
46 | 43 | ||
47 | { | 44 | { |
48 | const body = await server.users.getMyInfo() | 45 | const body = await server.users.getMyInfo() |
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index 5d354aad1..2d47c131b 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -610,7 +610,7 @@ describe('Test live', function () { | |||
610 | } | 610 | } |
611 | 611 | ||
612 | before(async function () { | 612 | before(async function () { |
613 | this.timeout(160000) | 613 | this.timeout(300000) |
614 | 614 | ||
615 | liveVideoId = await createLiveWrapper({ saveReplay: false, permanent: false }) | 615 | liveVideoId = await createLiveWrapper({ saveReplay: false, permanent: false }) |
616 | liveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: false }) | 616 | liveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: false }) |
@@ -654,7 +654,7 @@ describe('Test live', function () { | |||
654 | }) | 654 | }) |
655 | 655 | ||
656 | it('Should save a non permanent live replay', async function () { | 656 | it('Should save a non permanent live replay', async function () { |
657 | this.timeout(120000) | 657 | this.timeout(240000) |
658 | 658 | ||
659 | await commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) | 659 | await commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) |
660 | 660 | ||
diff --git a/server/tests/api/moderation/abuses.ts b/server/tests/api/moderation/abuses.ts index 7bf49c7ec..568fbefcf 100644 --- a/server/tests/api/moderation/abuses.ts +++ b/server/tests/api/moderation/abuses.ts | |||
@@ -168,7 +168,7 @@ describe('Test abuses', function () { | |||
168 | expect(abuse2.reporterAccount.name).to.equal('root') | 168 | expect(abuse2.reporterAccount.name).to.equal('root') |
169 | expect(abuse2.reporterAccount.host).to.equal(servers[0].host) | 169 | expect(abuse2.reporterAccount.host).to.equal(servers[0].host) |
170 | 170 | ||
171 | expect(abuse2.video.id).to.equal(servers[1].store.videoCreated.id) | 171 | expect(abuse2.video.uuid).to.equal(servers[1].store.videoCreated.uuid) |
172 | 172 | ||
173 | expect(abuse2.comment).to.be.null | 173 | expect(abuse2.comment).to.be.null |
174 | 174 | ||
@@ -530,7 +530,7 @@ describe('Test abuses', function () { | |||
530 | it('Should keep the comment abuse when deleting the comment', async function () { | 530 | it('Should keep the comment abuse when deleting the comment', async function () { |
531 | this.timeout(10000) | 531 | this.timeout(10000) |
532 | 532 | ||
533 | const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.id) | 533 | const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.uuid) |
534 | 534 | ||
535 | await servers[0].comments.delete({ videoId: servers[1].store.videoCreated.uuid, commentId: commentServer2.id }) | 535 | await servers[0].comments.delete({ videoId: servers[1].store.videoCreated.uuid, commentId: commentServer2.id }) |
536 | 536 | ||
diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts index a7cc529f8..a11289236 100644 --- a/server/tests/api/notifications/user-notifications.ts +++ b/server/tests/api/notifications/user-notifications.ts | |||
@@ -545,7 +545,7 @@ describe('Test user notifications', function () { | |||
545 | await servers[1].subscriptions.remove({ uri: 'user_1_channel@localhost:' + servers[0].port }) | 545 | await servers[1].subscriptions.remove({ uri: 'user_1_channel@localhost:' + servers[0].port }) |
546 | }) | 546 | }) |
547 | 547 | ||
548 | // PeerTube does not support accout -> account follows | 548 | // PeerTube does not support account -> account follows |
549 | // it('Should notify when a local account is following one of our channel', async function () { | 549 | // it('Should notify when a local account is following one of our channel', async function () { |
550 | // this.timeout(50000) | 550 | // this.timeout(50000) |
551 | // | 551 | // |
diff --git a/server/tests/api/server/contact-form.ts b/server/tests/api/server/contact-form.ts index 4f01f6fd5..d6165b293 100644 --- a/server/tests/api/server/contact-form.ts +++ b/server/tests/api/server/contact-form.ts | |||
@@ -61,7 +61,7 @@ describe('Test contact form', function () { | |||
61 | expect(email['text']).contains('my super message') | 61 | expect(email['text']).contains('my super message') |
62 | }) | 62 | }) |
63 | 63 | ||
64 | it('Should not have duplicated email adress in text message', async function () { | 64 | it('Should not have duplicated email address in text message', async function () { |
65 | const text = emails[0]['text'] as string | 65 | const text = emails[0]['text'] as string |
66 | 66 | ||
67 | const matches = text.match(/toto@example.com/g) | 67 | const matches = text.match(/toto@example.com/g) |
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts index fa2063536..0a1565faf 100644 --- a/server/tests/api/server/reverse-proxy.ts +++ b/server/tests/api/server/reverse-proxy.ts | |||
@@ -7,6 +7,7 @@ import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServ | |||
7 | 7 | ||
8 | describe('Test application behind a reverse proxy', function () { | 8 | describe('Test application behind a reverse proxy', function () { |
9 | let server: PeerTubeServer | 9 | let server: PeerTubeServer |
10 | let userAccessToken: string | ||
10 | let videoId: string | 11 | let videoId: string |
11 | 12 | ||
12 | before(async function () { | 13 | before(async function () { |
@@ -34,6 +35,8 @@ describe('Test application behind a reverse proxy', function () { | |||
34 | server = await createSingleServer(1, config) | 35 | server = await createSingleServer(1, config) |
35 | await setAccessTokensToServers([ server ]) | 36 | await setAccessTokensToServers([ server ]) |
36 | 37 | ||
38 | userAccessToken = await server.users.generateUserAndToken('user') | ||
39 | |||
37 | const { uuid } = await server.videos.upload() | 40 | const { uuid } = await server.videos.upload() |
38 | videoId = uuid | 41 | videoId = uuid |
39 | }) | 42 | }) |
@@ -93,7 +96,7 @@ describe('Test application behind a reverse proxy', function () { | |||
93 | it('Should rate limit logins', async function () { | 96 | it('Should rate limit logins', async function () { |
94 | const user = { username: 'root', password: 'fail' } | 97 | const user = { username: 'root', password: 'fail' } |
95 | 98 | ||
96 | for (let i = 0; i < 19; i++) { | 99 | for (let i = 0; i < 18; i++) { |
97 | await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 100 | await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
98 | } | 101 | } |
99 | 102 | ||
@@ -141,6 +144,12 @@ describe('Test application behind a reverse proxy', function () { | |||
141 | await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) | 144 | await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) |
142 | }) | 145 | }) |
143 | 146 | ||
147 | it('Should rate limit API calls with a user but not with an admin', async function () { | ||
148 | await server.videos.get({ id: videoId, token: userAccessToken, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) | ||
149 | |||
150 | await server.videos.get({ id: videoId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
151 | }) | ||
152 | |||
144 | after(async function () { | 153 | after(async function () { |
145 | await cleanupTests([ server ]) | 154 | await cleanupTests([ server ]) |
146 | }) | 155 | }) |
diff --git a/server/tests/api/server/services.ts b/server/tests/api/server/services.ts index 5fd2abda4..2f95f953b 100644 --- a/server/tests/api/server/services.ts +++ b/server/tests/api/server/services.ts | |||
@@ -13,6 +13,21 @@ describe('Test services', function () { | |||
13 | let playlistDisplayName: string | 13 | let playlistDisplayName: string |
14 | let video: Video | 14 | let video: Video |
15 | 15 | ||
16 | const urlSuffixes = [ | ||
17 | { | ||
18 | input: '', | ||
19 | output: '' | ||
20 | }, | ||
21 | { | ||
22 | input: '?param=1', | ||
23 | output: '' | ||
24 | }, | ||
25 | { | ||
26 | input: '?muted=1&warningTitle=0&toto=1', | ||
27 | output: '?muted=1&warningTitle=0' | ||
28 | } | ||
29 | ] | ||
30 | |||
16 | before(async function () { | 31 | before(async function () { |
17 | this.timeout(30000) | 32 | this.timeout(30000) |
18 | 33 | ||
@@ -52,14 +67,15 @@ describe('Test services', function () { | |||
52 | 67 | ||
53 | it('Should have a valid oEmbed video response', async function () { | 68 | it('Should have a valid oEmbed video response', async function () { |
54 | for (const basePath of [ '/videos/watch/', '/w/' ]) { | 69 | for (const basePath of [ '/videos/watch/', '/w/' ]) { |
55 | for (const suffix of [ '', '?param=1' ]) { | 70 | for (const suffix of urlSuffixes) { |
56 | const oembedUrl = server.url + basePath + video.uuid + suffix | 71 | const oembedUrl = server.url + basePath + video.uuid + suffix.input |
57 | 72 | ||
58 | const res = await server.services.getOEmbed({ oembedUrl }) | 73 | const res = await server.services.getOEmbed({ oembedUrl }) |
59 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' + | 74 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' + |
60 | `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + | 75 | `title="${video.name}" src="http://${server.host}/videos/embed/${video.uuid}${suffix.output}" ` + |
61 | 'frameborder="0" allowfullscreen></iframe>' | 76 | 'frameborder="0" allowfullscreen></iframe>' |
62 | const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath | 77 | |
78 | const expectedThumbnailUrl = 'http://' + server.host + video.previewPath | ||
63 | 79 | ||
64 | expect(res.body.html).to.equal(expectedHtml) | 80 | expect(res.body.html).to.equal(expectedHtml) |
65 | expect(res.body.title).to.equal(video.name) | 81 | expect(res.body.title).to.equal(video.name) |
@@ -75,12 +91,12 @@ describe('Test services', function () { | |||
75 | 91 | ||
76 | it('Should have a valid playlist oEmbed response', async function () { | 92 | it('Should have a valid playlist oEmbed response', async function () { |
77 | for (const basePath of [ '/videos/watch/playlist/', '/w/p/' ]) { | 93 | for (const basePath of [ '/videos/watch/playlist/', '/w/p/' ]) { |
78 | for (const suffix of [ '', '?param=1' ]) { | 94 | for (const suffix of urlSuffixes) { |
79 | const oembedUrl = server.url + basePath + playlistUUID + suffix | 95 | const oembedUrl = server.url + basePath + playlistUUID + suffix.input |
80 | 96 | ||
81 | const res = await server.services.getOEmbed({ oembedUrl }) | 97 | const res = await server.services.getOEmbed({ oembedUrl }) |
82 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' + | 98 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' + |
83 | `title="${playlistDisplayName}" src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` + | 99 | `title="${playlistDisplayName}" src="http://${server.host}/video-playlists/embed/${playlistUUID}${suffix.output}" ` + |
84 | 'frameborder="0" allowfullscreen></iframe>' | 100 | 'frameborder="0" allowfullscreen></iframe>' |
85 | 101 | ||
86 | expect(res.body.html).to.equal(expectedHtml) | 102 | expect(res.body.html).to.equal(expectedHtml) |
@@ -97,14 +113,14 @@ describe('Test services', function () { | |||
97 | 113 | ||
98 | it('Should have a valid oEmbed response with small max height query', async function () { | 114 | it('Should have a valid oEmbed response with small max height query', async function () { |
99 | for (const basePath of [ '/videos/watch/', '/w/' ]) { | 115 | for (const basePath of [ '/videos/watch/', '/w/' ]) { |
100 | const oembedUrl = 'http://localhost:' + server.port + basePath + video.uuid | 116 | const oembedUrl = 'http://' + server.host + basePath + video.uuid |
101 | const format = 'json' | 117 | const format = 'json' |
102 | const maxHeight = 50 | 118 | const maxHeight = 50 |
103 | const maxWidth = 50 | 119 | const maxWidth = 50 |
104 | 120 | ||
105 | const res = await server.services.getOEmbed({ oembedUrl, format, maxHeight, maxWidth }) | 121 | const res = await server.services.getOEmbed({ oembedUrl, format, maxHeight, maxWidth }) |
106 | const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts allow-popups" ' + | 122 | const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts allow-popups" ' + |
107 | `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + | 123 | `title="${video.name}" src="http://${server.host}/videos/embed/${video.uuid}" ` + |
108 | 'frameborder="0" allowfullscreen></iframe>' | 124 | 'frameborder="0" allowfullscreen></iframe>' |
109 | 125 | ||
110 | expect(res.body.html).to.equal(expectedHtml) | 126 | expect(res.body.html).to.equal(expectedHtml) |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 01b4c2eab..d15daeba5 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -380,7 +380,7 @@ describe('Test users', function () { | |||
380 | }) | 380 | }) |
381 | 381 | ||
382 | it('Should disable webtorrent, enable HLS, and update my quota', async function () { | 382 | it('Should disable webtorrent, enable HLS, and update my quota', async function () { |
383 | this.timeout(60000) | 383 | this.timeout(160000) |
384 | 384 | ||
385 | { | 385 | { |
386 | const config = await server.config.getCustomConfig() | 386 | const config = await server.config.getCustomConfig() |
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index 27b119f30..a0b6b01cf 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -16,3 +16,4 @@ import './video-schedule-update' | |||
16 | import './videos-common-filters' | 16 | import './videos-common-filters' |
17 | import './videos-history' | 17 | import './videos-history' |
18 | import './videos-overview' | 18 | import './videos-overview' |
19 | import './video-source' | ||
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index 6f495c42d..42e0cf431 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts | |||
@@ -478,6 +478,25 @@ describe('Test video channels', function () { | |||
478 | } | 478 | } |
479 | }) | 479 | }) |
480 | 480 | ||
481 | it('Should report correct total views count', async function () { | ||
482 | // check if there's the property | ||
483 | { | ||
484 | const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true }) | ||
485 | |||
486 | for (const channel of data) { | ||
487 | expect(channel).to.haveOwnProperty('totalViews') | ||
488 | expect(channel.totalViews).to.be.a('number') | ||
489 | } | ||
490 | } | ||
491 | |||
492 | // Check if the totalViews count can be updated | ||
493 | { | ||
494 | const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true }) | ||
495 | const channelWithView = data.find(channel => channel.id === servers[0].store.channel.id) | ||
496 | expect(channelWithView.totalViews).to.equal(2) | ||
497 | } | ||
498 | }) | ||
499 | |||
481 | it('Should report correct videos count', async function () { | 500 | it('Should report correct videos count', async function () { |
482 | const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true }) | 501 | const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true }) |
483 | 502 | ||
diff --git a/server/tests/api/videos/video-privacy.ts b/server/tests/api/videos/video-privacy.ts index 3051a443d..1073aee8c 100644 --- a/server/tests/api/videos/video-privacy.ts +++ b/server/tests/api/videos/video-privacy.ts | |||
@@ -162,7 +162,7 @@ describe('Test video privacy', function () { | |||
162 | }) | 162 | }) |
163 | 163 | ||
164 | it('Should not be able to get this unlisted video using its id', async function () { | 164 | it('Should not be able to get this unlisted video using its id', async function () { |
165 | await servers[1].videos.get({ id: unlistedVideo.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 165 | await servers[1].videos.get({ id: unlistedVideo.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) |
166 | }) | 166 | }) |
167 | 167 | ||
168 | it('Should be able to get this unlisted video using its uuid/shortUUID', async function () { | 168 | it('Should be able to get this unlisted video using its uuid/shortUUID', async function () { |
diff --git a/server/tests/api/videos/video-source.ts b/server/tests/api/videos/video-source.ts new file mode 100644 index 000000000..e34642300 --- /dev/null +++ b/server/tests/api/videos/video-source.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import 'mocha' | ||
2 | import * as chai from 'chai' | ||
3 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
4 | |||
5 | const expect = chai.expect | ||
6 | |||
7 | describe('Test video source', () => { | ||
8 | let server: PeerTubeServer = null | ||
9 | const fixture = 'video_short.webm' | ||
10 | |||
11 | before(async function () { | ||
12 | this.timeout(30000) | ||
13 | |||
14 | server = await createSingleServer(1) | ||
15 | await setAccessTokensToServers([ server ]) | ||
16 | }) | ||
17 | |||
18 | it('Should get the source filename with legacy upload', async function () { | ||
19 | this.timeout(30000) | ||
20 | |||
21 | const { uuid } = await server.videos.upload({ attributes: { name: 'my video', fixture }, mode: 'legacy' }) | ||
22 | |||
23 | const source = await server.videos.getSource({ id: uuid }) | ||
24 | expect(source.filename).to.equal(fixture) | ||
25 | }) | ||
26 | |||
27 | it('Should get the source filename with resumable upload', async function () { | ||
28 | this.timeout(30000) | ||
29 | |||
30 | const { uuid } = await server.videos.upload({ attributes: { name: 'my video', fixture }, mode: 'resumable' }) | ||
31 | |||
32 | const source = await server.videos.getSource({ id: uuid }) | ||
33 | expect(source.filename).to.equal(fixture) | ||
34 | }) | ||
35 | |||
36 | after(async function () { | ||
37 | await cleanupTests([ server ]) | ||
38 | }) | ||
39 | }) | ||
diff --git a/server/tests/helpers/core-utils.ts b/server/tests/helpers/core-utils.ts index fa0a71341..e3e5eb45c 100644 --- a/server/tests/helpers/core-utils.ts +++ b/server/tests/helpers/core-utils.ts | |||
@@ -6,47 +6,64 @@ import { snakeCase } from 'lodash' | |||
6 | import validator from 'validator' | 6 | import validator from 'validator' |
7 | import { getAverageBitrate, getMaxBitrate } from '@shared/core-utils' | 7 | import { getAverageBitrate, getMaxBitrate } from '@shared/core-utils' |
8 | import { VideoResolution } from '@shared/models' | 8 | import { VideoResolution } from '@shared/models' |
9 | import { objectConverter, parseBytes } from '../../helpers/core-utils' | 9 | import { objectConverter, parseBytes, parseDurationToMs } from '../../helpers/core-utils' |
10 | 10 | ||
11 | const expect = chai.expect | 11 | const expect = chai.expect |
12 | 12 | ||
13 | describe('Parse Bytes', function () { | 13 | describe('Parse Bytes', function () { |
14 | 14 | ||
15 | it('Should pass when given valid value', async function () { | 15 | it('Should pass on valid value', async function () { |
16 | // just return it | 16 | // just return it |
17 | expect(parseBytes(1024)).to.be.eq(1024) | 17 | expect(parseBytes(-1024)).to.equal(-1024) |
18 | expect(parseBytes(1048576)).to.be.eq(1048576) | 18 | expect(parseBytes(1024)).to.equal(1024) |
19 | expect(parseBytes('1024')).to.be.eq(1024) | 19 | expect(parseBytes(1048576)).to.equal(1048576) |
20 | expect(parseBytes('1048576')).to.be.eq(1048576) | 20 | expect(parseBytes('1024')).to.equal(1024) |
21 | expect(parseBytes('1048576')).to.equal(1048576) | ||
21 | 22 | ||
22 | // sizes | 23 | // sizes |
23 | expect(parseBytes('1B')).to.be.eq(1024) | 24 | expect(parseBytes('1B')).to.equal(1024) |
24 | expect(parseBytes('1MB')).to.be.eq(1048576) | 25 | expect(parseBytes('1MB')).to.equal(1048576) |
25 | expect(parseBytes('1GB')).to.be.eq(1073741824) | 26 | expect(parseBytes('1GB')).to.equal(1073741824) |
26 | expect(parseBytes('1TB')).to.be.eq(1099511627776) | 27 | expect(parseBytes('1TB')).to.equal(1099511627776) |
27 | 28 | ||
28 | expect(parseBytes('5GB')).to.be.eq(5368709120) | 29 | expect(parseBytes('5GB')).to.equal(5368709120) |
29 | expect(parseBytes('5TB')).to.be.eq(5497558138880) | 30 | expect(parseBytes('5TB')).to.equal(5497558138880) |
30 | 31 | ||
31 | expect(parseBytes('1024B')).to.be.eq(1048576) | 32 | expect(parseBytes('1024B')).to.equal(1048576) |
32 | expect(parseBytes('1024MB')).to.be.eq(1073741824) | 33 | expect(parseBytes('1024MB')).to.equal(1073741824) |
33 | expect(parseBytes('1024GB')).to.be.eq(1099511627776) | 34 | expect(parseBytes('1024GB')).to.equal(1099511627776) |
34 | expect(parseBytes('1024TB')).to.be.eq(1125899906842624) | 35 | expect(parseBytes('1024TB')).to.equal(1125899906842624) |
35 | 36 | ||
36 | // with whitespace | 37 | // with whitespace |
37 | expect(parseBytes('1 GB')).to.be.eq(1073741824) | 38 | expect(parseBytes('1 GB')).to.equal(1073741824) |
38 | expect(parseBytes('1\tGB')).to.be.eq(1073741824) | 39 | expect(parseBytes('1\tGB')).to.equal(1073741824) |
39 | 40 | ||
40 | // sum value | 41 | // sum value |
41 | expect(parseBytes('1TB 1024MB')).to.be.eq(1100585369600) | 42 | expect(parseBytes('1TB 1024MB')).to.equal(1100585369600) |
42 | expect(parseBytes('4GB 1024MB')).to.be.eq(5368709120) | 43 | expect(parseBytes('4GB 1024MB')).to.equal(5368709120) |
43 | expect(parseBytes('4TB 1024GB')).to.be.eq(5497558138880) | 44 | expect(parseBytes('4TB 1024GB')).to.equal(5497558138880) |
44 | expect(parseBytes('4TB 1024GB 0MB')).to.be.eq(5497558138880) | 45 | expect(parseBytes('4TB 1024GB 0MB')).to.equal(5497558138880) |
45 | expect(parseBytes('1024TB 1024GB 1024MB')).to.be.eq(1127000492212224) | 46 | expect(parseBytes('1024TB 1024GB 1024MB')).to.equal(1127000492212224) |
47 | }) | ||
48 | |||
49 | it('Should be invalid when given invalid value', async function () { | ||
50 | expect(parseBytes('6GB 1GB')).to.equal(6) | ||
51 | }) | ||
52 | }) | ||
53 | |||
54 | describe('Parse duration', function () { | ||
55 | |||
56 | it('Should pass when given valid value', async function () { | ||
57 | expect(parseDurationToMs(35)).to.equal(35) | ||
58 | expect(parseDurationToMs(-35)).to.equal(-35) | ||
59 | expect(parseDurationToMs('35 seconds')).to.equal(35 * 1000) | ||
60 | expect(parseDurationToMs('1 minute')).to.equal(60 * 1000) | ||
61 | expect(parseDurationToMs('1 hour')).to.equal(3600 * 1000) | ||
62 | expect(parseDurationToMs('35 hours')).to.equal(3600 * 35 * 1000) | ||
46 | }) | 63 | }) |
47 | 64 | ||
48 | it('Should be invalid when given invalid value', async function () { | 65 | it('Should be invalid when given invalid value', async function () { |
49 | expect(parseBytes('6GB 1GB')).to.be.eq(6) | 66 | expect(parseBytes('35m 5s')).to.equal(35) |
50 | }) | 67 | }) |
51 | }) | 68 | }) |
52 | 69 | ||
diff --git a/server/types/express.d.ts b/server/types/express.d.ts index 7cc13f21d..27e532c31 100644 --- a/server/types/express.d.ts +++ b/server/types/express.d.ts | |||
@@ -42,6 +42,7 @@ import { | |||
42 | MVideoThumbnail | 42 | MVideoThumbnail |
43 | } from './models' | 43 | } from './models' |
44 | import { Writable } from 'stream' | 44 | import { Writable } from 'stream' |
45 | import { MVideoSource } from './models/video/video-source' | ||
45 | 46 | ||
46 | declare module 'express' { | 47 | declare module 'express' { |
47 | export interface Request { | 48 | export interface Request { |
@@ -68,7 +69,7 @@ declare module 'express' { | |||
68 | } | UploadFileForCheck[] | 69 | } | UploadFileForCheck[] |
69 | 70 | ||
70 | // Upload file with a duration added by our middleware | 71 | // Upload file with a duration added by our middleware |
71 | export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size'> & { | 72 | export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size', 'originalname'> & { |
72 | duration: number | 73 | duration: number |
73 | } | 74 | } |
74 | 75 | ||
@@ -85,6 +86,7 @@ declare module 'express' { | |||
85 | duration: number | 86 | duration: number |
86 | path: string | 87 | path: string |
87 | filename: string | 88 | filename: string |
89 | originalname: string | ||
88 | } | 90 | } |
89 | 91 | ||
90 | // Extends Response with added functions and potential variables passed by middlewares | 92 | // Extends Response with added functions and potential variables passed by middlewares |
@@ -123,6 +125,8 @@ declare module 'express' { | |||
123 | 125 | ||
124 | videoShare?: MVideoShareActor | 126 | videoShare?: MVideoShareActor |
125 | 127 | ||
128 | videoSource?: MVideoSource | ||
129 | |||
126 | videoFile?: MVideoFile | 130 | videoFile?: MVideoFile |
127 | 131 | ||
128 | videoFileResumable?: EnhancedUploadXFile | 132 | videoFileResumable?: EnhancedUploadXFile |
diff --git a/server/types/models/video/video-source.ts b/server/types/models/video/video-source.ts new file mode 100644 index 000000000..0948f3b2e --- /dev/null +++ b/server/types/models/video/video-source.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
2 | |||
3 | export type MVideoSource = Omit<VideoSourceModel, 'Video'> | ||