aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/index.ts4
-rw-r--r--server/controllers/api/users/index.ts12
-rw-r--r--server/controllers/api/users/token.ts5
-rw-r--r--server/controllers/api/videos/view.ts2
-rw-r--r--server/controllers/feeds.ts10
-rw-r--r--server/controllers/lazy-static.ts21
-rw-r--r--server/helpers/audit-logger.ts12
-rw-r--r--server/helpers/core-utils.ts37
-rw-r--r--server/helpers/image-utils.ts2
-rw-r--r--server/helpers/upload.ts9
-rw-r--r--server/initializers/checker-before-init.ts5
-rw-r--r--server/initializers/config.ts9
-rw-r--r--server/initializers/constants.ts19
-rw-r--r--server/lib/activitypub/process/process-create.ts2
-rw-r--r--server/lib/activitypub/videos/shared/creator.ts2
-rw-r--r--server/lib/activitypub/videos/shared/object-to-model-attributes.ts2
-rw-r--r--server/lib/client-html.ts6
-rw-r--r--server/lib/emailer.ts2
-rw-r--r--server/lib/job-queue/handlers/move-to-object-storage.ts17
-rw-r--r--server/lib/job-queue/job-queue.ts11
-rw-r--r--server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts2
-rw-r--r--server/lib/redis.ts2
-rw-r--r--server/lib/schedulers/geo-ip-update-scheduler.ts2
-rw-r--r--server/lib/signup.ts2
-rw-r--r--server/lib/video.ts24
-rw-r--r--server/lib/views/shared/video-viewer-counters.ts8
-rw-r--r--server/lib/views/shared/video-viewer-stats.ts6
-rw-r--r--server/lib/views/shared/video-views.ts17
-rw-r--r--server/lib/views/video-views-manager.ts4
-rw-r--r--server/middlewares/index.ts1
-rw-r--r--server/middlewares/rate-limiter.ts31
-rw-r--r--server/middlewares/validators/sort.ts4
-rw-r--r--server/middlewares/validators/users.ts2
-rw-r--r--server/middlewares/validators/videos/video-view.ts13
-rw-r--r--server/models/abuse/abuse-query-builder.ts2
-rw-r--r--server/models/shared/abstract-run-query.ts2
-rw-r--r--server/models/user/user.ts6
-rw-r--r--server/models/utils.ts24
-rw-r--r--server/models/video/video-channel.ts13
-rw-r--r--server/tests/api/check-params/abuses.ts4
-rw-r--r--server/tests/api/live/live.ts4
-rw-r--r--server/tests/api/moderation/abuses.ts4
-rw-r--r--server/tests/api/notifications/user-notifications.ts2
-rw-r--r--server/tests/api/search/search-index.ts8
-rw-r--r--server/tests/api/server/contact-form.ts2
-rw-r--r--server/tests/api/server/reverse-proxy.ts11
-rw-r--r--server/tests/api/users/users.ts2
-rw-r--r--server/tests/api/videos/video-channels.ts19
-rw-r--r--server/tests/helpers/core-utils.ts65
49 files changed, 324 insertions, 151 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 @@
1import cors from 'cors' 1import cors from 'cors'
2import express from 'express' 2import express from 'express'
3import RateLimit from 'express-rate-limit' 3import { buildRateLimiter } from '@server/middlewares'
4import { HttpStatusCode } from '../../../shared/models' 4import { HttpStatusCode } from '../../../shared/models'
5import { badRequest } from '../../helpers/express-utils' 5import { badRequest } from '../../helpers/express-utils'
6import { CONFIG } from '../../initializers/config' 6import { CONFIG } from '../../initializers/config'
@@ -29,7 +29,7 @@ apiRouter.use(cors({
29 credentials: true 29 credentials: true
30})) 30}))
31 31
32const apiRateLimiter = RateLimit({ 32const 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 @@
1import express from 'express' 1import express from 'express'
2import RateLimit from 'express-rate-limit'
3import { tokensRouter } from '@server/controllers/api/users/token' 2import { tokensRouter } from '@server/controllers/api/users/token'
4import { Hooks } from '@server/lib/plugins/hooks' 3import { Hooks } from '@server/lib/plugins/hooks'
5import { OAuthTokenModel } from '@server/models/oauth/oauth-token' 4import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
@@ -17,9 +16,11 @@ import { Notifier } from '../../../lib/notifier'
17import { Redis } from '../../../lib/redis' 16import { Redis } from '../../../lib/redis'
18import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' 17import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
19import { 18import {
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'
38import { 38import {
@@ -54,13 +54,13 @@ import { myVideoPlaylistsRouter } from './my-video-playlists'
54 54
55const auditLogger = auditLoggerFactory('users') 55const auditLogger = auditLoggerFactory('users')
56 56
57const signupRateLimiter = RateLimit({ 57const 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
63const askSendEmailLimiter = RateLimit({ 63const 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
279async function listUsers (req: express.Request, res: express.Response) { 279async 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 @@
1import express from 'express' 1import express from 'express'
2import RateLimit from 'express-rate-limit'
3import { logger } from '@server/helpers/logger' 2import { logger } from '@server/helpers/logger'
4import { CONFIG } from '@server/initializers/config' 3import { CONFIG } from '@server/initializers/config'
5import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' 4import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
6import { handleOAuthToken } from '@server/lib/auth/oauth' 5import { handleOAuthToken } from '@server/lib/auth/oauth'
7import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' 6import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
8import { Hooks } from '@server/lib/plugins/hooks' 7import { Hooks } from '@server/lib/plugins/hooks'
9import { asyncMiddleware, authenticate, openapiOperationDoc } from '@server/middlewares' 8import { asyncMiddleware, authenticate, buildRateLimiter, openapiOperationDoc } from '@server/middlewares'
10import { buildUUID } from '@shared/extra-utils' 9import { buildUUID } from '@shared/extra-utils'
11import { ScopedToken } from '@shared/models/users/user-scoped-token' 10import { ScopedToken } from '@shared/models/users/user-scoped-token'
12 11
13const tokensRouter = express.Router() 12const tokensRouter = express.Router()
14 13
15const loginRateLimiter = RateLimit({ 14const 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/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
28async function viewVideo (req: express.Request, res: express.Response) { 28async 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 c929a6726..9eb31ed93 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,13 +1,13 @@
1import express from 'express' 1import express from 'express'
2import { Feed } from '@peertube/feed'
3import { extname } from 'path' 2import { extname } from 'path'
3import { Feed } from '@peertube/feed'
4import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown' 4import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown'
5import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
6import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' 6import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils'
7import { VideoInclude } from '@shared/models' 7import { VideoInclude } from '@shared/models'
8import { buildNSFWFilter } from '../helpers/express-utils' 8import { buildNSFWFilter } from '../helpers/express-utils'
9import { CONFIG } from '../initializers/config' 9import { CONFIG } from '../initializers/config'
10import { FEEDS, MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' 10import { MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
11import { 11import {
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..8a180b5bc 100644
--- a/server/controllers/lazy-static.ts
+++ b/server/controllers/lazy-static.ts
@@ -1,9 +1,10 @@
1import cors from 'cors' 1import cors from 'cors'
2import express from 'express' 2import express from 'express'
3import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' 3import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
4import { MActorImage } from '@server/types/models'
4import { HttpStatusCode } from '../../shared/models/http/http-error-codes' 5import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
5import { logger } from '../helpers/logger' 6import { logger } from '../helpers/logger'
6import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' 7import { ACTOR_IMAGES_SIZE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
7import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' 8import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
8import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/local-actor' 9import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/local-actor'
9import { asyncMiddleware } from '../middlewares' 10import { asyncMiddleware } from '../middlewares'
@@ -67,10 +68,7 @@ async function getActorImage (req: express.Request, res: express.Response, next:
67 await pushActorImageProcessInQueue({ 68 await pushActorImageProcessInQueue({
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
106function 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
108async function getPreview (req: express.Request, res: express.Response) { 117async 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/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]
122class VideoAuditView extends EntityAuditView { 122class 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]
133class VideoImportAuditView extends EntityAuditView { 133class 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]
152class CommentAuditView extends EntityAuditView { 152class 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]
181class UserAuditView extends EntityAuditView { 181class 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]
207class VideoChannelAuditView extends EntityAuditView { 207class 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]
219class AbuseAuditView extends EntityAuditView { 219class 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 = {
56export function parseDurationToMs (duration: number | string): number { 56export 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
77export function parseBytes (value: string | number): number { 78export 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/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 @@
1import { join } from 'path' 1import { join } from 'path'
2import { JobQueue } from '@server/lib/job-queue'
3import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants' 2import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants'
4 3
5function getResumableUploadPath (filename?: string) { 4function 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
11function 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
18export { 12export {
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 9201f95b3..f54ce9506 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -58,7 +58,7 @@ const WEBSERVER = {
58 58
59// Sortable columns per schema 59// Sortable columns per schema
60const SORTABLE_COLUMNS = { 60const 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
214const AP_CLEANER = { 214const 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,12 +734,14 @@ const VIDEO_LIVE = {
734const MEMOIZE_TTL = { 734const 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
741const MEMOIZE_LENGTH = { 742const MEMOIZE_LENGTH = {
742 INFO_HASH_EXISTS: 200 743 INFO_HASH_EXISTS: 200,
744 VIDEO_DURATION: 200
743} 745}
744 746
745const QUEUE_CONCURRENCY = { 747const QUEUE_CONCURRENCY = {
@@ -769,12 +771,6 @@ const CUSTOM_HTML_TAG_COMMENTS = {
769 SERVER_CONFIG: '<!-- server config -->' 771 SERVER_CONFIG: '<!-- server config -->'
770} 772}
771 773
772// ---------------------------------------------------------------------------
773
774const FEEDS = {
775 COUNT: 20
776}
777
778const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000 774const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000
779const LOG_FILENAME = 'peertube.log' 775const LOG_FILENAME = 'peertube.log'
780const AUDIT_LOG_FILENAME = 'peertube-audit.log' 776const AUDIT_LOG_FILENAME = 'peertube-audit.log'
@@ -818,7 +814,7 @@ const STATS_TIMESERIE = {
818// --------------------------------------------------------------------------- 814// ---------------------------------------------------------------------------
819 815
820// Special constants for a test instance 816// Special constants for a test instance
821if (isTestInstance() === true) { 817if (isTestInstance() === true && process.env.PRODUCTION_CONSTANTS !== 'true') {
822 PRIVATE_RSA_KEY_SIZE = 1024 818 PRIVATE_RSA_KEY_SIZE = 1024
823 819
824 ACTOR_FOLLOW_SCORE.BASE = 20 820 ACTOR_FOLLOW_SCORE.BASE = 20
@@ -942,7 +938,6 @@ export {
942 ROUTE_CACHE_LIFETIME, 938 ROUTE_CACHE_LIFETIME,
943 SORTABLE_COLUMNS, 939 SORTABLE_COLUMNS,
944 HLS_STREAMING_PLAYLIST_DIRECTORY, 940 HLS_STREAMING_PLAYLIST_DIRECTORY,
945 FEEDS,
946 JOB_TTL, 941 JOB_TTL,
947 DEFAULT_THEME_NAME, 942 DEFAULT_THEME_NAME,
948 NSFW_POLICY_TYPES, 943 NSFW_POLICY_TYPES,
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'
30import { getActivityStreamDuration } from './activitypub/activity' 30import { getActivityStreamDuration } from './activitypub/activity'
31import { getBiggestActorImage } from './actor-image' 31import { getBiggestActorImage } from './actor-image'
32import { ServerConfigManager } from './server-config-manager' 32import { ServerConfigManager } from './server-config-manager'
33import { isTestInstance } from '@server/helpers/core-utils'
33 34
34type Tags = { 35type 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
57export 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
62async function moveWebTorrentFiles (video: MVideoWithAllFiles) { 71async 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'
33import { processActorKeys } from './handlers/actor-keys' 33import { processActorKeys } from './handlers/actor-keys'
34import { processEmail } from './handlers/email' 34import { processEmail } from './handlers/email'
35import { processManageVideoTorrent } from './handlers/manage-video-torrent' 35import { processManageVideoTorrent } from './handlers/manage-video-torrent'
36import { processMoveToObjectStorage } from './handlers/move-to-object-storage' 36import { onMoveToObjectStorageFailure, processMoveToObjectStorage } from './handlers/move-to-object-storage'
37import { processVideoFileImport } from './handlers/video-file-import' 37import { processVideoFileImport } from './handlers/video-file-import'
38import { processVideoImport } from './handlers/video-import' 38import { processVideoImport } from './handlers/video-import'
39import { processVideoLiveEnding } from './handlers/video-live-ending' 39import { 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
91const errorHandlers: { [id in JobType]?: (job: Job, err: any) => Promise<any> } = {
92 'move-to-object-storage': onMoveToObjectStorageFailure
93}
94
91const jobTypes: JobType[] = [ 95const 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/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
5import { UserNotificationType } from '@shared/models' 5import { UserNotificationType } from '@shared/models'
6import { AbstractNotification } from '../common/abstract-notification' 6import { AbstractNotification } from '../common/abstract-notification'
7 7
8export type NewAbuseMessagePayload = { 8type 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..158f3c080 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -308,7 +308,7 @@ class Redis {
308 return this.deleteKey('resumable-upload-' + uploadId) 308 return this.deleteKey('resumable-upload-' + uploadId)
309 } 309 }
310 310
311 /* ************ AP ressource unavailability ************ */ 311 /* ************ AP resource unavailability ************ */
312 312
313 async addAPUnavailability (url: string) { 313 async addAPUnavailability (url: string) {
314 const key = this.generateAPUnavailabilityKey(url) 314 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/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 @@
1import { UploadFiles } from 'express' 1import { UploadFiles } from 'express'
2import { Transaction } from 'sequelize/types' 2import { Transaction } from 'sequelize/types'
3import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY } from '@server/initializers/constants' 3import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY, MEMOIZE_LENGTH, MEMOIZE_TTL } from '@server/initializers/constants'
4import { TagModel } from '@server/models/video/tag' 4import { TagModel } from '@server/models/video/tag'
5import { VideoModel } from '@server/models/video/video' 5import { VideoModel } from '@server/models/video/video'
6import { VideoJobInfoModel } from '@server/models/video/video-job-info' 6import { VideoJobInfoModel } from '@server/models/video/video-job-info'
@@ -10,6 +10,7 @@ import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState, VideoTranscodingP
10import { CreateJobOptions, JobQueue } from './job-queue/job-queue' 10import { CreateJobOptions, JobQueue } from './job-queue/job-queue'
11import { updateVideoMiniatureFromExisting } from './thumbnail' 11import { updateVideoMiniatureFromExisting } from './thumbnail'
12import { CONFIG } from '@server/initializers/config' 12import { CONFIG } from '@server/initializers/config'
13import memoizee from 'memoizee'
13 14
14function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { 15function 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
154async 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
164const getCachedVideoDuration = memoizee(getVideoDuration, {
165 promise: true,
166 max: MEMOIZE_LENGTH.VIDEO_DURATION,
167 maxAge: MEMOIZE_TTL.VIDEO_DURATION
168})
169
170// ---------------------------------------------------------------------------
171
153export { 172export {
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'
5import { PeerTubeSocket } from '@server/lib/peertube-socket' 5import { PeerTubeSocket } from '@server/lib/peertube-socket'
6import { getServerActor } from '@server/models/application/application' 6import { getServerActor } from '@server/models/application/application'
7import { VideoModel } from '@server/models/video/video' 7import { VideoModel } from '@server/models/video/video'
8import { MVideo } from '@server/types/models' 8import { MVideo, MVideoImmutable } from '@server/types/models'
9import { buildUUID, sha256 } from '@shared/extra-utils' 9import { buildUUID, sha256 } from '@shared/extra-utils'
10 10
11const lTags = loggerTagsFactory('views') 11const 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'
10import { VideoModel } from '@server/models/video/video' 10import { VideoModel } from '@server/models/video/video'
11import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer' 11import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
12import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section' 12import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section'
13import { MVideo } from '@server/types/models' 13import { MVideo, MVideoImmutable } from '@server/types/models'
14import { VideoViewEvent } from '@shared/models' 14import { VideoViewEvent } from '@shared/models'
15 15
16const lTags = loggerTagsFactory('views') 16const 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 @@
1import { logger, loggerTagsFactory } from '@server/helpers/logger' 1import { logger, loggerTagsFactory } from '@server/helpers/logger'
2import { sendView } from '@server/lib/activitypub/send/send-view' 2import { sendView } from '@server/lib/activitypub/send/send-view'
3import { getCachedVideoDuration } from '@server/lib/video'
3import { getServerActor } from '@server/models/application/application' 4import { getServerActor } from '@server/models/application/application'
4import { MVideo } from '@server/types/models' 5import { MVideo, MVideoImmutable } from '@server/types/models'
5import { buildUUID } from '@shared/extra-utils' 6import { buildUUID } from '@shared/extra-utils'
6import { Redis } from '../../redis' 7import { Redis } from '../../redis'
7 8
@@ -10,7 +11,7 @@ const lTags = loggerTagsFactory('views')
10export class VideoViews { 11export 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 @@
1import { logger, loggerTagsFactory } from '@server/helpers/logger' 1import { logger, loggerTagsFactory } from '@server/helpers/logger'
2import { MVideo } from '@server/types/models' 2import { MVideo, MVideoImmutable } from '@server/types/models'
3import { VideoViewEvent } from '@shared/models' 3import { VideoViewEvent } from '@shared/models'
4import { VideoViewerCounters, VideoViewerStats, VideoViews } from './shared' 4import { 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/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'
4export * from './async' 4export * from './async'
5export * from './auth' 5export * from './auth'
6export * from './pagination' 6export * from './pagination'
7export * from './rate-limiter'
7export * from './robots' 8export * from './robots'
8export * from './servers' 9export * from './servers'
9export * from './sort' 10export * 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 @@
1import { UserRole } from '@shared/models'
2import RateLimit from 'express-rate-limit'
3import { optionalAuthenticate } from './auth'
4
5const whitelistRoles = new Set([ UserRole.ADMINISTRATOR, UserRole.MODERATOR ])
6
7function 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
29export {
30 buildRateLimiter
31}
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
31const usersSortValidator = checkSortFactory(SORTABLE_COLUMNS.USERS) 31const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS)
32const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS) 32const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS)
33const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ]) 33const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ])
34const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES) 34const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES)
@@ -59,7 +59,7 @@ const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CH
59// --------------------------------------------------------------------------- 59// ---------------------------------------------------------------------------
60 60
61export { 61export {
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/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'
6import { exists, isIdValid, isIntOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' 6import { exists, isIdValid, isIntOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
7import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' 8import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
9import { getCachedVideoDuration } from '@server/lib/video'
9 10
10const getVideoLocalViewerValidator = [ 11const 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/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..a25551ecd 100644
--- a/server/models/user/user.ts
+++ b/server/models/user/user.ts
@@ -66,7 +66,7 @@ import { ActorModel } from '../actor/actor'
66import { ActorFollowModel } from '../actor/actor-follow' 66import { ActorFollowModel } from '../actor/actor-follow'
67import { ActorImageModel } from '../actor/actor-image' 67import { ActorImageModel } from '../actor/actor-image'
68import { OAuthTokenModel } from '../oauth/oauth-token' 68import { OAuthTokenModel } from '../oauth/oauth-token'
69import { getSort, throwIfNotValid } from '../utils' 69import { getAdminUsersSort, throwIfNotValid } from '../utils'
70import { VideoModel } from '../video/video' 70import { VideoModel } from '../video/video'
71import { VideoChannelModel } from '../video/video-channel' 71import { VideoChannelModel } from '../video/video-channel'
72import { VideoImportModel } from '../video/video-import' 72import { VideoImportModel } from '../video/video-import'
@@ -461,7 +461,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
461 return this.count() 461 return this.count()
462 } 462 }
463 463
464 static listForApi (parameters: { 464 static listForAdminApi (parameters: {
465 start: number 465 start: number
466 count: number 466 count: number
467 sort: string 467 sort: string
@@ -497,7 +497,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
497 const query: FindOptions = { 497 const query: FindOptions = {
498 offset: start, 498 offset: start,
499 limit: count, 499 limit: count,
500 order: getSort(sort), 500 order: getAdminUsersSort(sort),
501 where 501 where
502 } 502 }
503 503
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
21function 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
23function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { 40function 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
103function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { 120function 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/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/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/live/live.ts b/server/tests/api/live/live.ts
index 9b8fbe3e2..b9caf394d 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/search/search-index.ts b/server/tests/api/search/search-index.ts
index 287abe455..53b91e1cb 100644
--- a/server/tests/api/search/search-index.ts
+++ b/server/tests/api/search/search-index.ts
@@ -136,14 +136,14 @@ describe('Test index search', function () {
136 expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft') 136 expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft')
137 // TODO: remove, deprecated in 4.2 137 // TODO: remove, deprecated in 4.2
138 expect(video.account.avatar).to.exist 138 expect(video.account.avatar).to.exist
139 expect(video.account.avatars.length).to.equal(1, 'Account should have one avatar image') 139 expect(video.account.avatars.length).to.equal(2, 'Account should have one avatar image')
140 140
141 expect(video.channel.host).to.equal('framatube.org') 141 expect(video.channel.host).to.equal('framatube.org')
142 expect(video.channel.name).to.equal('joinpeertube') 142 expect(video.channel.name).to.equal('joinpeertube')
143 expect(video.channel.url).to.equal('https://framatube.org/video-channels/joinpeertube') 143 expect(video.channel.url).to.equal('https://framatube.org/video-channels/joinpeertube')
144 // TODO: remove, deprecated in 4.2 144 // TODO: remove, deprecated in 4.2
145 expect(video.channel.avatar).to.exist 145 expect(video.channel.avatar).to.exist
146 expect(video.channel.avatars.length).to.equal(1, 'Channel should have one avatar image') 146 expect(video.channel.avatars.length).to.equal(2, 'Channel should have one avatar image')
147 } 147 }
148 148
149 const baseSearch: VideosSearchQuery = { 149 const baseSearch: VideosSearchQuery = {
@@ -322,7 +322,7 @@ describe('Test index search', function () {
322 expect(videoChannel.host).to.equal('framatube.org') 322 expect(videoChannel.host).to.equal('framatube.org')
323 // TODO: remove, deprecated in 4.2 323 // TODO: remove, deprecated in 4.2
324 expect(videoChannel.avatar).to.exist 324 expect(videoChannel.avatar).to.exist
325 expect(videoChannel.avatars.length).to.equal(1, 'Channel should have two avatar images') 325 expect(videoChannel.avatars.length).to.equal(2, 'Channel should have two avatar images')
326 expect(videoChannel.displayName).to.exist 326 expect(videoChannel.displayName).to.exist
327 327
328 expect(videoChannel.ownerAccount.url).to.equal('https://framatube.org/accounts/framasoft') 328 expect(videoChannel.ownerAccount.url).to.equal('https://framatube.org/accounts/framasoft')
@@ -330,7 +330,7 @@ describe('Test index search', function () {
330 expect(videoChannel.ownerAccount.host).to.equal('framatube.org') 330 expect(videoChannel.ownerAccount.host).to.equal('framatube.org')
331 // TODO: remove, deprecated in 4.2 331 // TODO: remove, deprecated in 4.2
332 expect(videoChannel.ownerAccount.avatar).to.exist 332 expect(videoChannel.ownerAccount.avatar).to.exist
333 expect(videoChannel.ownerAccount.avatars.length).to.equal(1, 'Account should have two avatar images') 333 expect(videoChannel.ownerAccount.avatars.length).to.equal(2, 'Account should have two avatar images')
334 } 334 }
335 335
336 it('Should make a simple search and not have results', async function () { 336 it('Should make a simple search and not have results', async function () {
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
8describe('Test application behind a reverse proxy', function () { 8describe('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/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/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/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'
6import validator from 'validator' 6import validator from 'validator'
7import { getAverageBitrate, getMaxBitrate } from '@shared/core-utils' 7import { getAverageBitrate, getMaxBitrate } from '@shared/core-utils'
8import { VideoResolution } from '@shared/models' 8import { VideoResolution } from '@shared/models'
9import { objectConverter, parseBytes } from '../../helpers/core-utils' 9import { objectConverter, parseBytes, parseDurationToMs } from '../../helpers/core-utils'
10 10
11const expect = chai.expect 11const expect = chai.expect
12 12
13describe('Parse Bytes', function () { 13describe('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
54describe('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