aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/accounts.ts4
-rw-r--r--server/controllers/api/config.ts4
-rw-r--r--server/controllers/api/users/me.ts6
-rw-r--r--server/controllers/api/video-channel.ts6
-rw-r--r--server/controllers/api/videos/import.ts6
-rw-r--r--server/controllers/api/videos/index.ts10
-rw-r--r--server/controllers/bots.ts101
-rw-r--r--server/controllers/index.ts1
-rw-r--r--server/controllers/static.ts9
-rw-r--r--server/helpers/express-utils.ts4
-rw-r--r--server/helpers/image-utils.ts14
-rw-r--r--server/helpers/requests.ts9
-rw-r--r--server/helpers/utils.ts6
-rw-r--r--server/helpers/webtorrent.ts6
-rw-r--r--server/helpers/youtube-dl.ts4
-rw-r--r--server/initializers/checker-before-init.ts1
-rw-r--r--server/initializers/constants.ts6
-rw-r--r--server/lib/activitypub/actor.ts4
-rw-r--r--server/lib/activitypub/process/process-update.ts2
-rw-r--r--server/lib/activitypub/videos.ts24
-rw-r--r--server/lib/emailer.ts7
-rw-r--r--server/lib/job-queue/handlers/activitypub-refresher.ts3
-rw-r--r--server/lib/job-queue/handlers/video-file.ts4
-rw-r--r--server/lib/job-queue/handlers/video-import.ts9
-rw-r--r--server/lib/job-queue/handlers/video-views.ts4
-rw-r--r--server/lib/redis.ts9
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts4
-rw-r--r--server/models/account/account.ts21
-rw-r--r--server/models/redundancy/video-redundancy.ts4
-rw-r--r--server/models/video/video-channel.ts21
-rw-r--r--server/models/video/video.ts30
-rw-r--r--server/tests/api/redundancy/redundancy.ts27
-rw-r--r--server/tests/cli/index.ts1
-rw-r--r--server/tests/misc-endpoints.ts72
34 files changed, 347 insertions, 96 deletions
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 86ef2aed1..a69a83acf 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -74,10 +74,10 @@ async function listVideoAccountChannels (req: express.Request, res: express.Resp
74 74
75async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 75async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
76 const account: AccountModel = res.locals.account 76 const account: AccountModel = res.locals.account
77 const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 77 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
78 78
79 const resultList = await VideoModel.listForApi({ 79 const resultList = await VideoModel.listForApi({
80 actorId, 80 followerActorId,
81 start: req.query.start, 81 start: req.query.start,
82 count: req.query.count, 82 count: req.query.count,
83 sort: req.query.sort, 83 sort: req.query.sort,
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 5233e9f68..d65e321e9 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -11,6 +11,7 @@ import { ClientHtml } from '../../lib/client-html'
11import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' 11import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
12import { remove, writeJSON } from 'fs-extra' 12import { remove, writeJSON } from 'fs-extra'
13import { getServerCommit } from '../../helpers/utils' 13import { getServerCommit } from '../../helpers/utils'
14import { Emailer } from '../../lib/emailer'
14 15
15const packageJSON = require('../../../../package.json') 16const packageJSON = require('../../../../package.json')
16const configRouter = express.Router() 17const configRouter = express.Router()
@@ -61,6 +62,9 @@ async function getConfig (req: express.Request, res: express.Response) {
61 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS 62 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
62 } 63 }
63 }, 64 },
65 email: {
66 enabled: Emailer.Instance.isEnabled()
67 },
64 serverVersion: packageJSON.version, 68 serverVersion: packageJSON.version,
65 serverCommit, 69 serverCommit,
66 signup: { 70 signup: {
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 82299747d..d2456346b 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -42,7 +42,7 @@ import { AccountModel } from '../../../models/account/account'
42 42
43const auditLogger = auditLoggerFactory('users-me') 43const auditLogger = auditLoggerFactory('users-me')
44 44
45const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) 45const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
46 46
47const meRouter = express.Router() 47const meRouter = express.Router()
48 48
@@ -238,7 +238,7 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res
238 nsfw: buildNSFWFilter(res, req.query.nsfw), 238 nsfw: buildNSFWFilter(res, req.query.nsfw),
239 filter: req.query.filter as VideoFilter, 239 filter: req.query.filter as VideoFilter,
240 withFiles: false, 240 withFiles: false,
241 actorId: user.Account.Actor.id, 241 followerActorId: user.Account.Actor.id,
242 user 242 user
243 }) 243 })
244 244
@@ -348,7 +348,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
348 return res.sendStatus(204) 348 return res.sendStatus(204)
349} 349}
350 350
351async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { 351async function updateMyAvatar (req: express.Request, res: express.Response) {
352 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] 352 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
353 const user: UserModel = res.locals.oauth.token.user 353 const user: UserModel = res.locals.oauth.token.user
354 const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) 354 const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 9bf3c5fd8..fd143a139 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -32,7 +32,7 @@ import { resetSequelizeInstance } from '../../helpers/database-utils'
32import { UserModel } from '../../models/account/user' 32import { UserModel } from '../../models/account/user'
33 33
34const auditLogger = auditLoggerFactory('channels') 34const auditLogger = auditLoggerFactory('channels')
35const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) 35const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
36 36
37const videoChannelRouter = express.Router() 37const videoChannelRouter = express.Router()
38 38
@@ -202,10 +202,10 @@ async function getVideoChannel (req: express.Request, res: express.Response, nex
202 202
203async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 203async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
204 const videoChannelInstance: VideoChannelModel = res.locals.videoChannel 204 const videoChannelInstance: VideoChannelModel = res.locals.videoChannel
205 const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 205 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
206 206
207 const resultList = await VideoModel.listForApi({ 207 const resultList = await VideoModel.listForApi({
208 actorId, 208 followerActorId,
209 start: req.query.start, 209 start: req.query.start,
210 count: req.query.count, 210 count: req.query.count,
211 sort: req.query.sort, 211 sort: req.query.sort,
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index 398fd5a7f..f27d648c7 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -37,9 +37,9 @@ const reqVideoFileImport = createReqFiles(
37 [ 'thumbnailfile', 'previewfile', 'torrentfile' ], 37 [ 'thumbnailfile', 'previewfile', 'torrentfile' ],
38 Object.assign({}, TORRENT_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), 38 Object.assign({}, TORRENT_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT),
39 { 39 {
40 thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, 40 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
41 previewfile: CONFIG.STORAGE.PREVIEWS_DIR, 41 previewfile: CONFIG.STORAGE.TMP_DIR,
42 torrentfile: CONFIG.STORAGE.TORRENTS_DIR 42 torrentfile: CONFIG.STORAGE.TMP_DIR
43 } 43 }
44) 44)
45 45
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 3d1b2e1a2..4e4697ef4 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -67,17 +67,17 @@ const reqVideoFileAdd = createReqFiles(
67 [ 'videofile', 'thumbnailfile', 'previewfile' ], 67 [ 'videofile', 'thumbnailfile', 'previewfile' ],
68 Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), 68 Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT),
69 { 69 {
70 videofile: CONFIG.STORAGE.VIDEOS_DIR, 70 videofile: CONFIG.STORAGE.TMP_DIR,
71 thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, 71 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
72 previewfile: CONFIG.STORAGE.PREVIEWS_DIR 72 previewfile: CONFIG.STORAGE.TMP_DIR
73 } 73 }
74) 74)
75const reqVideoFileUpdate = createReqFiles( 75const reqVideoFileUpdate = createReqFiles(
76 [ 'thumbnailfile', 'previewfile' ], 76 [ 'thumbnailfile', 'previewfile' ],
77 IMAGE_MIMETYPE_EXT, 77 IMAGE_MIMETYPE_EXT,
78 { 78 {
79 thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, 79 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
80 previewfile: CONFIG.STORAGE.PREVIEWS_DIR 80 previewfile: CONFIG.STORAGE.TMP_DIR
81 } 81 }
82) 82)
83 83
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts
new file mode 100644
index 000000000..2db86a2d8
--- /dev/null
+++ b/server/controllers/bots.ts
@@ -0,0 +1,101 @@
1import * as express from 'express'
2import { asyncMiddleware } from '../middlewares'
3import { CONFIG, ROUTE_CACHE_LIFETIME } from '../initializers'
4import * as sitemapModule from 'sitemap'
5import { logger } from '../helpers/logger'
6import { VideoModel } from '../models/video/video'
7import { VideoChannelModel } from '../models/video/video-channel'
8import { AccountModel } from '../models/account/account'
9import { cacheRoute } from '../middlewares/cache'
10import { buildNSFWFilter } from '../helpers/express-utils'
11import { truncate } from 'lodash'
12
13const botsRouter = express.Router()
14
15// Special route that add OpenGraph and oEmbed tags
16// Do not use a template engine for a so little thing
17botsRouter.use('/sitemap.xml',
18 asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP)),
19 asyncMiddleware(getSitemap)
20)
21
22// ---------------------------------------------------------------------------
23
24export {
25 botsRouter
26}
27
28// ---------------------------------------------------------------------------
29
30async function getSitemap (req: express.Request, res: express.Response) {
31 let urls = getSitemapBasicUrls()
32
33 urls = urls.concat(await getSitemapLocalVideoUrls())
34 urls = urls.concat(await getSitemapVideoChannelUrls())
35 urls = urls.concat(await getSitemapAccountUrls())
36
37 const sitemap = sitemapModule.createSitemap({
38 hostname: CONFIG.WEBSERVER.URL,
39 urls: urls
40 })
41
42 sitemap.toXML((err, xml) => {
43 if (err) {
44 logger.error('Cannot generate sitemap.', { err })
45 return res.sendStatus(500)
46 }
47
48 res.header('Content-Type', 'application/xml')
49 res.send(xml)
50 })
51}
52
53async function getSitemapVideoChannelUrls () {
54 const rows = await VideoChannelModel.listLocalsForSitemap('createdAt')
55
56 return rows.map(channel => ({
57 url: CONFIG.WEBSERVER.URL + '/video-channels/' + channel.Actor.preferredUsername
58 }))
59}
60
61async function getSitemapAccountUrls () {
62 const rows = await AccountModel.listLocalsForSitemap('createdAt')
63
64 return rows.map(channel => ({
65 url: CONFIG.WEBSERVER.URL + '/accounts/' + channel.Actor.preferredUsername
66 }))
67}
68
69async function getSitemapLocalVideoUrls () {
70 const resultList = await VideoModel.listForApi({
71 start: 0,
72 count: undefined,
73 sort: 'createdAt',
74 includeLocalVideos: true,
75 nsfw: buildNSFWFilter(),
76 filter: 'local',
77 withFiles: false
78 })
79
80 return resultList.data.map(v => ({
81 url: CONFIG.WEBSERVER.URL + '/videos/watch/' + v.uuid,
82 video: [
83 {
84 title: v.name,
85 // Sitemap description should be < 2000 characters
86 description: truncate(v.description || v.name, { length: 2000, omission: '...' }),
87 player_loc: CONFIG.WEBSERVER.URL + '/videos/embed/' + v.uuid,
88 thumbnail_loc: CONFIG.WEBSERVER.URL + v.getThumbnailStaticPath()
89 }
90 ]
91 }))
92}
93
94function getSitemapBasicUrls () {
95 const paths = [
96 '/about/instance',
97 '/videos/local'
98 ]
99
100 return paths.map(p => ({ url: CONFIG.WEBSERVER.URL + p }))
101}
diff --git a/server/controllers/index.ts b/server/controllers/index.ts
index 197fa897a..a88a03c79 100644
--- a/server/controllers/index.ts
+++ b/server/controllers/index.ts
@@ -6,3 +6,4 @@ export * from './services'
6export * from './static' 6export * from './static'
7export * from './webfinger' 7export * from './webfinger'
8export * from './tracker' 8export * from './tracker'
9export * from './bots'
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 75e30353c..55e7392a1 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -34,13 +34,18 @@ staticRouter.use(
34) 34)
35 35
36// Videos path for webseeding 36// Videos path for webseeding
37const videosPhysicalPath = CONFIG.STORAGE.VIDEOS_DIR
38staticRouter.use( 37staticRouter.use(
39 STATIC_PATHS.WEBSEED, 38 STATIC_PATHS.WEBSEED,
40 cors(), 39 cors(),
41 express.static(videosPhysicalPath) 40 express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video
42) 41)
43staticRouter.use( 42staticRouter.use(
43 STATIC_PATHS.REDUNDANCY,
44 cors(),
45 express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video
46)
47
48staticRouter.use(
44 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', 49 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
45 asyncMiddleware(videosGetValidator), 50 asyncMiddleware(videosGetValidator),
46 asyncMiddleware(downloadVideoFile) 51 asyncMiddleware(downloadVideoFile)
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
index 162fe2244..9a72ee96d 100644
--- a/server/helpers/express-utils.ts
+++ b/server/helpers/express-utils.ts
@@ -7,12 +7,12 @@ import { extname } from 'path'
7import { isArray } from './custom-validators/misc' 7import { isArray } from './custom-validators/misc'
8import { UserModel } from '../models/account/user' 8import { UserModel } from '../models/account/user'
9 9
10function buildNSFWFilter (res: express.Response, paramNSFW?: string) { 10function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
11 if (paramNSFW === 'true') return true 11 if (paramNSFW === 'true') return true
12 if (paramNSFW === 'false') return false 12 if (paramNSFW === 'false') return false
13 if (paramNSFW === 'both') return undefined 13 if (paramNSFW === 'both') return undefined
14 14
15 if (res.locals.oauth) { 15 if (res && res.locals.oauth) {
16 const user: UserModel = res.locals.oauth.token.User 16 const user: UserModel = res.locals.oauth.token.User
17 17
18 // User does not want NSFW videos 18 // User does not want NSFW videos
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts
index da3285b13..e43ea3f1d 100644
--- a/server/helpers/image-utils.ts
+++ b/server/helpers/image-utils.ts
@@ -1,6 +1,7 @@
1import 'multer' 1import 'multer'
2import * as sharp from 'sharp' 2import * as sharp from 'sharp'
3import { move, remove } from 'fs-extra' 3import { readFile, remove } from 'fs-extra'
4import { logger } from './logger'
4 5
5async function processImage ( 6async function processImage (
6 physicalFile: { path: string }, 7 physicalFile: { path: string },
@@ -11,14 +12,11 @@ async function processImage (
11 throw new Error('Sharp needs an input path different that the output path.') 12 throw new Error('Sharp needs an input path different that the output path.')
12 } 13 }
13 14
14 const sharpInstance = sharp(physicalFile.path) 15 logger.debug('Processing image %s to %s.', physicalFile.path, destination)
15 const metadata = await sharpInstance.metadata()
16 16
17 // No need to resize 17 // Avoid sharp cache
18 if (metadata.width === newSize.width && metadata.height === newSize.height) { 18 const buf = await readFile(physicalFile.path)
19 await move(physicalFile.path, destination, { overwrite: true }) 19 const sharpInstance = sharp(buf)
20 return
21 }
22 20
23 await remove(destination) 21 await remove(destination)
24 22
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index 805930a9f..3fc776f1a 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -1,8 +1,9 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { createWriteStream } from 'fs-extra' 2import { createWriteStream } from 'fs-extra'
3import * as request from 'request' 3import * as request from 'request'
4import { ACTIVITY_PUB } from '../initializers' 4import { ACTIVITY_PUB, CONFIG } from '../initializers'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { join } from 'path'
6 7
7function doRequest <T> ( 8function doRequest <T> (
8 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } 9 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
@@ -28,11 +29,11 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U
28 }) 29 })
29} 30}
30 31
31async function downloadImage (url: string, destPath: string, size: { width: number, height: number }) { 32async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) {
32 const tmpPath = destPath + '.tmp' 33 const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName)
33
34 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) 34 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath)
35 35
36 const destPath = join(destDir, destName)
36 await processImage({ path: tmpPath }, destPath, size) 37 await processImage({ path: tmpPath }, destPath, size)
37} 38}
38 39
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 5c9d6fe2f..9b89e3e61 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -46,11 +46,11 @@ const getServerActor = memoizee(async function () {
46 return actor 46 return actor
47}) 47})
48 48
49function generateVideoTmpPath (target: string | ParseTorrent) { 49function generateVideoImportTmpPath (target: string | ParseTorrent) {
50 const id = typeof target === 'string' ? target : target.infoHash 50 const id = typeof target === 'string' ? target : target.infoHash
51 51
52 const hash = sha256(id) 52 const hash = sha256(id)
53 return join(CONFIG.STORAGE.VIDEOS_DIR, hash + '-import.mp4') 53 return join(CONFIG.STORAGE.TMP_DIR, hash + '-import.mp4')
54} 54}
55 55
56function getSecureTorrentName (originalName: string) { 56function getSecureTorrentName (originalName: string) {
@@ -103,6 +103,6 @@ export {
103 getSecureTorrentName, 103 getSecureTorrentName,
104 getServerActor, 104 getServerActor,
105 getServerCommit, 105 getServerCommit,
106 generateVideoTmpPath, 106 generateVideoImportTmpPath,
107 getUUIDFromFilename 107 getUUIDFromFilename
108} 108}
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts
index ce35b87da..3c9a0b96a 100644
--- a/server/helpers/webtorrent.ts
+++ b/server/helpers/webtorrent.ts
@@ -1,5 +1,5 @@
1import { logger } from './logger' 1import { logger } from './logger'
2import { generateVideoTmpPath } from './utils' 2import { generateVideoImportTmpPath } from './utils'
3import * as WebTorrent from 'webtorrent' 3import * as WebTorrent from 'webtorrent'
4import { createWriteStream, ensureDir, remove } from 'fs-extra' 4import { createWriteStream, ensureDir, remove } from 'fs-extra'
5import { CONFIG } from '../initializers' 5import { CONFIG } from '../initializers'
@@ -9,10 +9,10 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName
9 const id = target.magnetUri || target.torrentName 9 const id = target.magnetUri || target.torrentName
10 let timer 10 let timer
11 11
12 const path = generateVideoTmpPath(id) 12 const path = generateVideoImportTmpPath(id)
13 logger.info('Importing torrent video %s', id) 13 logger.info('Importing torrent video %s', id)
14 14
15 const directoryPath = join(CONFIG.STORAGE.VIDEOS_DIR, 'import') 15 const directoryPath = join(CONFIG.STORAGE.TMP_DIR, 'webtorrent')
16 await ensureDir(directoryPath) 16 await ensureDir(directoryPath)
17 17
18 return new Promise<string>((res, rej) => { 18 return new Promise<string>((res, rej) => {
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts
index 2a5663042..b74351b42 100644
--- a/server/helpers/youtube-dl.ts
+++ b/server/helpers/youtube-dl.ts
@@ -1,7 +1,7 @@
1import { truncate } from 'lodash' 1import { truncate } from 'lodash'
2import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' 2import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers'
3import { logger } from './logger' 3import { logger } from './logger'
4import { generateVideoTmpPath } from './utils' 4import { generateVideoImportTmpPath } from './utils'
5import { join } from 'path' 5import { join } from 'path'
6import { root } from './core-utils' 6import { root } from './core-utils'
7import { ensureDir, writeFile, remove } from 'fs-extra' 7import { ensureDir, writeFile, remove } from 'fs-extra'
@@ -40,7 +40,7 @@ function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo>
40} 40}
41 41
42function downloadYoutubeDLVideo (url: string, timeout: number) { 42function downloadYoutubeDLVideo (url: string, timeout: number) {
43 const path = generateVideoTmpPath(url) 43 const path = generateVideoImportTmpPath(url)
44 let timer 44 let timer
45 45
46 logger.info('Importing youtubeDL video %s', url) 46 logger.info('Importing youtubeDL video %s', url)
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 9dfb5d68c..b51c7cfba 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -12,6 +12,7 @@ function checkMissedConfig () {
12 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', 12 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max',
13 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', 13 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',
14 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', 14 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache',
15 'storage.redundancy', 'storage.tmp',
15 'log.level', 16 'log.level',
16 'user.video_quota', 'user.video_quota_daily', 17 'user.video_quota', 'user.video_quota_daily',
17 'cache.previews.size', 'admin.email', 18 'cache.previews.size', 'admin.email',
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index aa243859c..6b798875c 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -61,6 +61,7 @@ const OAUTH_LIFETIME = {
61const ROUTE_CACHE_LIFETIME = { 61const ROUTE_CACHE_LIFETIME = {
62 FEEDS: '15 minutes', 62 FEEDS: '15 minutes',
63 ROBOTS: '2 hours', 63 ROBOTS: '2 hours',
64 SITEMAP: '1 day',
64 SECURITYTXT: '2 hours', 65 SECURITYTXT: '2 hours',
65 NODEINFO: '10 minutes', 66 NODEINFO: '10 minutes',
66 DNT_POLICY: '1 week', 67 DNT_POLICY: '1 week',
@@ -185,9 +186,11 @@ const CONFIG = {
185 FROM_ADDRESS: config.get<string>('smtp.from_address') 186 FROM_ADDRESS: config.get<string>('smtp.from_address')
186 }, 187 },
187 STORAGE: { 188 STORAGE: {
189 TMP_DIR: buildPath(config.get<string>('storage.tmp')),
188 AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), 190 AVATARS_DIR: buildPath(config.get<string>('storage.avatars')),
189 LOG_DIR: buildPath(config.get<string>('storage.logs')), 191 LOG_DIR: buildPath(config.get<string>('storage.logs')),
190 VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), 192 VIDEOS_DIR: buildPath(config.get<string>('storage.videos')),
193 REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')),
191 THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), 194 THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')),
192 PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), 195 PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')),
193 CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), 196 CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')),
@@ -569,6 +572,7 @@ const STATIC_PATHS = {
569 THUMBNAILS: '/static/thumbnails/', 572 THUMBNAILS: '/static/thumbnails/',
570 TORRENTS: '/static/torrents/', 573 TORRENTS: '/static/torrents/',
571 WEBSEED: '/static/webseed/', 574 WEBSEED: '/static/webseed/',
575 REDUNDANCY: '/static/redundancy/',
572 AVATARS: '/static/avatars/', 576 AVATARS: '/static/avatars/',
573 VIDEO_CAPTIONS: '/static/video-captions/' 577 VIDEO_CAPTIONS: '/static/video-captions/'
574} 578}
@@ -773,7 +777,7 @@ function buildVideosRedundancy (objs: any[]): VideosRedundancy[] {
773 if (!objs) return [] 777 if (!objs) return []
774 778
775 return objs.map(obj => { 779 return objs.map(obj => {
776 return Object.assign(obj, { 780 return Object.assign({}, obj, {
777 minLifetime: parseDuration(obj.min_lifetime), 781 minLifetime: parseDuration(obj.min_lifetime),
778 size: bytes.parse(obj.size), 782 size: bytes.parse(obj.size),
779 minViews: obj.min_views 783 minViews: obj.min_views
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 504263c99..bbe48833d 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -178,9 +178,7 @@ async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
178 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] 178 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
179 179
180 const avatarName = uuidv4() + extension 180 const avatarName = uuidv4() + extension
181 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) 181 await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE)
182
183 await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE)
184 182
185 return avatarName 183 return avatarName
186 } 184 }
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 03831a00e..c6b42d846 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -51,7 +51,7 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate)
51 return undefined 51 return undefined
52 } 52 }
53 53
54 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id }) 54 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id, allowRefresh: false })
55 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) 55 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
56 56
57 const updateOptions = { 57 const updateOptions = {
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 998f90330..3d17e6846 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -95,9 +95,8 @@ function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Fu
95 95
96function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { 96function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) {
97 const thumbnailName = video.getThumbnailName() 97 const thumbnailName = video.getThumbnailName()
98 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
99 98
100 return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE) 99 return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE)
101} 100}
102 101
103function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 102function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
@@ -158,25 +157,30 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid
158async function getOrCreateVideoAndAccountAndChannel (options: { 157async function getOrCreateVideoAndAccountAndChannel (options: {
159 videoObject: VideoTorrentObject | string, 158 videoObject: VideoTorrentObject | string,
160 syncParam?: SyncParam, 159 syncParam?: SyncParam,
161 fetchType?: VideoFetchByUrlType 160 fetchType?: VideoFetchByUrlType,
161 allowRefresh?: boolean // true by default
162}) { 162}) {
163 // Default params 163 // Default params
164 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 164 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
165 const fetchType = options.fetchType || 'all' 165 const fetchType = options.fetchType || 'all'
166 const allowRefresh = options.allowRefresh !== false
166 167
167 // Get video url 168 // Get video url
168 const videoUrl = getAPUrl(options.videoObject) 169 const videoUrl = getAPUrl(options.videoObject)
169 170
170 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) 171 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
171 if (videoFromDatabase) { 172 if (videoFromDatabase) {
172 const refreshOptions = {
173 video: videoFromDatabase,
174 fetchedType: fetchType,
175 syncParam
176 }
177 173
178 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) 174 if (allowRefresh === true) {
179 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } }) 175 const refreshOptions = {
176 video: videoFromDatabase,
177 fetchedType: fetchType,
178 syncParam
179 }
180
181 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
182 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } })
183 }
180 184
181 return { video: videoFromDatabase } 185 return { video: videoFromDatabase }
182 } 186 }
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 9327792fb..074d4ad44 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -14,6 +14,7 @@ class Emailer {
14 private static instance: Emailer 14 private static instance: Emailer
15 private initialized = false 15 private initialized = false
16 private transporter: Transporter 16 private transporter: Transporter
17 private enabled = false
17 18
18 private constructor () {} 19 private constructor () {}
19 20
@@ -50,6 +51,8 @@ class Emailer {
50 tls, 51 tls,
51 auth 52 auth
52 }) 53 })
54
55 this.enabled = true
53 } else { 56 } else {
54 if (!isTestInstance()) { 57 if (!isTestInstance()) {
55 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!') 58 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
@@ -57,6 +60,10 @@ class Emailer {
57 } 60 }
58 } 61 }
59 62
63 isEnabled () {
64 return this.enabled
65 }
66
60 async checkConnectionOrDie () { 67 async checkConnectionOrDie () {
61 if (!this.transporter) return 68 if (!this.transporter) return
62 69
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts
index 7752b3b40..671b0f487 100644
--- a/server/lib/job-queue/handlers/activitypub-refresher.ts
+++ b/server/lib/job-queue/handlers/activitypub-refresher.ts
@@ -10,7 +10,8 @@ export type RefreshPayload = {
10 10
11async function refreshAPObject (job: Bull.Job) { 11async function refreshAPObject (job: Bull.Job) {
12 const payload = job.data as RefreshPayload 12 const payload = job.data as RefreshPayload
13 logger.info('Processing AP refresher in job %d.', job.id) 13
14 logger.info('Processing AP refresher in job %d for video %s.', job.id, payload.videoUrl)
14 15
15 if (payload.type === 'video') return refreshAPVideo(payload.videoUrl) 16 if (payload.type === 'video') return refreshAPVideo(payload.videoUrl)
16} 17}
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts
index adc0a2a15..ddbf6d1c2 100644
--- a/server/lib/job-queue/handlers/video-file.ts
+++ b/server/lib/job-queue/handlers/video-file.ts
@@ -1,5 +1,5 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { VideoResolution, VideoState } from '../../../../shared' 2import { VideoResolution, VideoState, Job } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { VideoModel } from '../../../models/video/video' 4import { VideoModel } from '../../../models/video/video'
5import { JobQueue } from '../job-queue' 5import { JobQueue } from '../job-queue'
@@ -111,7 +111,7 @@ async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boole
111 ) 111 )
112 112
113 if (resolutionsEnabled.length !== 0) { 113 if (resolutionsEnabled.length !== 0) {
114 const tasks: Bluebird<any>[] = [] 114 const tasks: Bluebird<Bull.Job<any>>[] = []
115 115
116 for (const resolution of resolutionsEnabled) { 116 for (const resolution of resolutionsEnabled) {
117 const dataInput = { 117 const dataInput = {
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 4de901c0c..51a0b5faf 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -7,7 +7,7 @@ import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } fro
7import { extname, join } from 'path' 7import { extname, join } from 'path'
8import { VideoFileModel } from '../../../models/video/video-file' 8import { VideoFileModel } from '../../../models/video/video-file'
9import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' 9import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
10import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests' 10import { downloadImage } from '../../../helpers/requests'
11import { VideoState } from '../../../../shared' 11import { VideoState } from '../../../../shared'
12import { JobQueue } from '../index' 12import { JobQueue } from '../index'
13import { federateVideoIfNeeded } from '../../activitypub' 13import { federateVideoIfNeeded } from '../../activitypub'
@@ -109,6 +109,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
109 let tempVideoPath: string 109 let tempVideoPath: string
110 let videoDestFile: string 110 let videoDestFile: string
111 let videoFile: VideoFileModel 111 let videoFile: VideoFileModel
112
112 try { 113 try {
113 // Download video from youtubeDL 114 // Download video from youtubeDL
114 tempVideoPath = await downloader() 115 tempVideoPath = await downloader()
@@ -144,8 +145,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
144 // Process thumbnail 145 // Process thumbnail
145 if (options.downloadThumbnail) { 146 if (options.downloadThumbnail) {
146 if (options.thumbnailUrl) { 147 if (options.thumbnailUrl) {
147 const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName()) 148 await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName(), THUMBNAILS_SIZE)
148 await downloadImage(options.thumbnailUrl, destThumbnailPath, THUMBNAILS_SIZE)
149 } else { 149 } else {
150 await videoImport.Video.createThumbnail(videoFile) 150 await videoImport.Video.createThumbnail(videoFile)
151 } 151 }
@@ -156,8 +156,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
156 // Process preview 156 // Process preview
157 if (options.downloadPreview) { 157 if (options.downloadPreview) {
158 if (options.thumbnailUrl) { 158 if (options.thumbnailUrl) {
159 const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName()) 159 await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName(), PREVIEWS_SIZE)
160 await downloadImage(options.thumbnailUrl, destPreviewPath, PREVIEWS_SIZE)
161 } else { 160 } else {
162 await videoImport.Video.createPreview(videoFile) 161 await videoImport.Video.createPreview(videoFile)
163 } 162 }
diff --git a/server/lib/job-queue/handlers/video-views.ts b/server/lib/job-queue/handlers/video-views.ts
index 038ef43e2..fa1fd13b3 100644
--- a/server/lib/job-queue/handlers/video-views.ts
+++ b/server/lib/job-queue/handlers/video-views.ts
@@ -23,9 +23,7 @@ async function processVideosViews () {
23 for (const videoId of videoIds) { 23 for (const videoId of videoIds) {
24 try { 24 try {
25 const views = await Redis.Instance.getVideoViews(videoId, hour) 25 const views = await Redis.Instance.getVideoViews(videoId, hour)
26 if (isNaN(views)) { 26 if (views) {
27 logger.error('Cannot process videos views of video %d in hour %d: views number is NaN (%s).', videoId, hour, views)
28 } else {
29 logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour) 27 logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour)
30 28
31 try { 29 try {
diff --git a/server/lib/redis.ts b/server/lib/redis.ts
index abd75d512..3e25e6a2c 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -121,7 +121,14 @@ class Redis {
121 const key = this.generateVideoViewKey(videoId, hour) 121 const key = this.generateVideoViewKey(videoId, hour)
122 122
123 const valueString = await this.getValue(key) 123 const valueString = await this.getValue(key)
124 return parseInt(valueString, 10) 124 const valueInt = parseInt(valueString, 10)
125
126 if (isNaN(valueInt)) {
127 logger.error('Cannot get videos views of video %d in hour %d: views number is NaN (%s).', videoId, hour, valueString)
128 return undefined
129 }
130
131 return valueInt
125 } 132 }
126 133
127 async getVideosIdViewed (hour: number) { 134 async getVideosIdViewed (hour: number) {
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index 8b7f33539..2a99a665d 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -145,13 +145,13 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
145 145
146 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) 146 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT)
147 147
148 const destPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(file)) 148 const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file))
149 await rename(tmpPath, destPath) 149 await rename(tmpPath, destPath)
150 150
151 const createdModel = await VideoRedundancyModel.create({ 151 const createdModel = await VideoRedundancyModel.create({
152 expiresOn: this.buildNewExpiration(redundancy.minLifetime), 152 expiresOn: this.buildNewExpiration(redundancy.minLifetime),
153 url: getVideoCacheFileActivityPubUrl(file), 153 url: getVideoCacheFileActivityPubUrl(file),
154 fileUrl: video.getVideoFileUrl(file, CONFIG.WEBSERVER.URL), 154 fileUrl: video.getVideoRedundancyUrl(file, CONFIG.WEBSERVER.URL),
155 strategy: redundancy.strategy, 155 strategy: redundancy.strategy,
156 videoFileId: file.id, 156 videoFileId: file.id,
157 actorId: serverActor.id 157 actorId: serverActor.id
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 5a237d733..a99e9b1ad 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -241,6 +241,27 @@ export class AccountModel extends Model<AccountModel> {
241 }) 241 })
242 } 242 }
243 243
244 static listLocalsForSitemap (sort: string) {
245 const query = {
246 attributes: [ ],
247 offset: 0,
248 order: getSort(sort),
249 include: [
250 {
251 attributes: [ 'preferredUsername', 'serverId' ],
252 model: ActorModel.unscoped(),
253 where: {
254 serverId: null
255 }
256 }
257 ]
258 }
259
260 return AccountModel
261 .unscoped()
262 .findAll(query)
263 }
264
244 toFormattedJSON (): Account { 265 toFormattedJSON (): Account {
245 const actor = this.Actor.toFormattedJSON() 266 const actor = this.Actor.toFormattedJSON()
246 const account = { 267 const account = {
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 9de4356b4..dd37dad22 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -15,7 +15,7 @@ import {
15import { ActorModel } from '../activitypub/actor' 15import { ActorModel } from '../activitypub/actor'
16import { getVideoSort, throwIfNotValid } from '../utils' 16import { getVideoSort, throwIfNotValid } from '../utils'
17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' 17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers' 18import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, VIDEO_EXT_MIMETYPE } from '../../initializers'
19import { VideoFileModel } from '../video/video-file' 19import { VideoFileModel } from '../video/video-file'
20import { getServerActor } from '../../helpers/utils' 20import { getServerActor } from '../../helpers/utils'
21import { VideoModel } from '../video/video' 21import { VideoModel } from '../video/video'
@@ -124,7 +124,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
124 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` 124 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
125 logger.info('Removing duplicated video file %s.', logIdentifier) 125 logger.info('Removing duplicated video file %s.', logIdentifier)
126 126
127 videoFile.Video.removeFile(videoFile) 127 videoFile.Video.removeFile(videoFile, true)
128 .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) 128 .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
129 129
130 return undefined 130 return undefined
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index f4586917e..86bf0461a 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -233,6 +233,27 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
233 }) 233 })
234 } 234 }
235 235
236 static listLocalsForSitemap (sort: string) {
237 const query = {
238 attributes: [ ],
239 offset: 0,
240 order: getSort(sort),
241 include: [
242 {
243 attributes: [ 'preferredUsername', 'serverId' ],
244 model: ActorModel.unscoped(),
245 where: {
246 serverId: null
247 }
248 }
249 ]
250 }
251
252 return VideoChannelModel
253 .unscoped()
254 .findAll(query)
255 }
256
236 static searchForApi (options: { 257 static searchForApi (options: {
237 actorId: number 258 actorId: number
238 search: string 259 search: string
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 0f18d9f0c..adef37937 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -140,7 +140,7 @@ type ForAPIOptions = {
140 140
141type AvailableForListIDsOptions = { 141type AvailableForListIDsOptions = {
142 serverAccountId: number 142 serverAccountId: number
143 actorId: number 143 followerActorId: number
144 includeLocalVideos: boolean 144 includeLocalVideos: boolean
145 filter?: VideoFilter 145 filter?: VideoFilter
146 categoryOneOf?: number[] 146 categoryOneOf?: number[]
@@ -315,7 +315,7 @@ type AvailableForListIDsOptions = {
315 query.include.push(videoChannelInclude) 315 query.include.push(videoChannelInclude)
316 } 316 }
317 317
318 if (options.actorId) { 318 if (options.followerActorId) {
319 let localVideosReq = '' 319 let localVideosReq = ''
320 if (options.includeLocalVideos === true) { 320 if (options.includeLocalVideos === true) {
321 localVideosReq = ' UNION ALL ' + 321 localVideosReq = ' UNION ALL ' +
@@ -327,7 +327,7 @@ type AvailableForListIDsOptions = {
327 } 327 }
328 328
329 // Force actorId to be a number to avoid SQL injections 329 // Force actorId to be a number to avoid SQL injections
330 const actorIdNumber = parseInt(options.actorId.toString(), 10) 330 const actorIdNumber = parseInt(options.followerActorId.toString(), 10)
331 query.where[ 'id' ][ Sequelize.Op.and ].push({ 331 query.where[ 'id' ][ Sequelize.Op.and ].push({
332 [ Sequelize.Op.in ]: Sequelize.literal( 332 [ Sequelize.Op.in ]: Sequelize.literal(
333 '(' + 333 '(' +
@@ -985,7 +985,7 @@ export class VideoModel extends Model<VideoModel> {
985 filter?: VideoFilter, 985 filter?: VideoFilter,
986 accountId?: number, 986 accountId?: number,
987 videoChannelId?: number, 987 videoChannelId?: number,
988 actorId?: number 988 followerActorId?: number
989 trendingDays?: number, 989 trendingDays?: number,
990 user?: UserModel 990 user?: UserModel
991 }, countVideos = true) { 991 }, countVideos = true) {
@@ -1008,11 +1008,11 @@ export class VideoModel extends Model<VideoModel> {
1008 1008
1009 const serverActor = await getServerActor() 1009 const serverActor = await getServerActor()
1010 1010
1011 // actorId === null has a meaning, so just check undefined 1011 // followerActorId === null has a meaning, so just check undefined
1012 const actorId = options.actorId !== undefined ? options.actorId : serverActor.id 1012 const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id
1013 1013
1014 const queryOptions = { 1014 const queryOptions = {
1015 actorId, 1015 followerActorId,
1016 serverAccountId: serverActor.Account.id, 1016 serverAccountId: serverActor.Account.id,
1017 nsfw: options.nsfw, 1017 nsfw: options.nsfw,
1018 categoryOneOf: options.categoryOneOf, 1018 categoryOneOf: options.categoryOneOf,
@@ -1118,7 +1118,7 @@ export class VideoModel extends Model<VideoModel> {
1118 1118
1119 const serverActor = await getServerActor() 1119 const serverActor = await getServerActor()
1120 const queryOptions = { 1120 const queryOptions = {
1121 actorId: serverActor.id, 1121 followerActorId: serverActor.id,
1122 serverAccountId: serverActor.Account.id, 1122 serverAccountId: serverActor.Account.id,
1123 includeLocalVideos: options.includeLocalVideos, 1123 includeLocalVideos: options.includeLocalVideos,
1124 nsfw: options.nsfw, 1124 nsfw: options.nsfw,
@@ -1273,11 +1273,11 @@ export class VideoModel extends Model<VideoModel> {
1273 // threshold corresponds to how many video the field should have to be returned 1273 // threshold corresponds to how many video the field should have to be returned
1274 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1274 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1275 const serverActor = await getServerActor() 1275 const serverActor = await getServerActor()
1276 const actorId = serverActor.id 1276 const followerActorId = serverActor.id
1277 1277
1278 const scopeOptions: AvailableForListIDsOptions = { 1278 const scopeOptions: AvailableForListIDsOptions = {
1279 serverAccountId: serverActor.Account.id, 1279 serverAccountId: serverActor.Account.id,
1280 actorId, 1280 followerActorId,
1281 includeLocalVideos: true 1281 includeLocalVideos: true
1282 } 1282 }
1283 1283
@@ -1538,8 +1538,10 @@ export class VideoModel extends Model<VideoModel> {
1538 .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) 1538 .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err }))
1539 } 1539 }
1540 1540
1541 removeFile (videoFile: VideoFileModel) { 1541 removeFile (videoFile: VideoFileModel, isRedundancy = false) {
1542 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) 1542 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
1543
1544 const filePath = join(baseDir, this.getVideoFilename(videoFile))
1543 return remove(filePath) 1545 return remove(filePath)
1544 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) 1546 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
1545 } 1547 }
@@ -1617,6 +1619,10 @@ export class VideoModel extends Model<VideoModel> {
1617 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) 1619 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
1618 } 1620 }
1619 1621
1622 getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1623 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile)
1624 }
1625
1620 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 1626 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1621 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) 1627 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
1622 } 1628 }
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index a8a2f305f..5b29a503a 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -136,7 +136,7 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st
136 if (!videoUUID) videoUUID = video1Server2UUID 136 if (!videoUUID) videoUUID = video1Server2UUID
137 137
138 const webseeds = [ 138 const webseeds = [
139 'http://localhost:9001/static/webseed/' + videoUUID, 139 'http://localhost:9001/static/redundancy/' + videoUUID,
140 'http://localhost:9002/static/webseed/' + videoUUID 140 'http://localhost:9002/static/webseed/' + videoUUID
141 ] 141 ]
142 142
@@ -148,20 +148,23 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st
148 for (const file of video.files) { 148 for (const file of video.files) {
149 checkMagnetWebseeds(file, webseeds, server) 149 checkMagnetWebseeds(file, webseeds, server)
150 150
151 // Only servers 1 and 2 have the video 151 await makeGetRequest({
152 if (server.serverNumber !== 3) { 152 url: servers[0].url,
153 await makeGetRequest({ 153 statusCodeExpected: 200,
154 url: server.url, 154 path: '/static/redundancy/' + `${videoUUID}-${file.resolution.id}.mp4`,
155 statusCodeExpected: 200, 155 contentType: null
156 path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, 156 })
157 contentType: null 157 await makeGetRequest({
158 }) 158 url: servers[1].url,
159 } 159 statusCodeExpected: 200,
160 path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`,
161 contentType: null
162 })
160 } 163 }
161 } 164 }
162 165
163 for (const directory of [ 'test1', 'test2' ]) { 166 for (const directory of [ 'test1/redundancy', 'test2/videos' ]) {
164 const files = await readdir(join(root(), directory, 'videos')) 167 const files = await readdir(join(root(), directory))
165 expect(files).to.have.length.at.least(4) 168 expect(files).to.have.length.at.least(4)
166 169
167 for (const resolution of [ 240, 360, 480, 720 ]) { 170 for (const resolution of [ 240, 360, 480, 720 ]) {
diff --git a/server/tests/cli/index.ts b/server/tests/cli/index.ts
index 6201314ce..c6b7ec078 100644
--- a/server/tests/cli/index.ts
+++ b/server/tests/cli/index.ts
@@ -1,6 +1,7 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './create-import-video-file-job' 2import './create-import-video-file-job'
3import './create-transcoding-job' 3import './create-transcoding-job'
4import './optimize-old-videos'
4import './peertube' 5import './peertube'
5import './reset-password' 6import './reset-password'
6import './update-host' 7import './update-host'
diff --git a/server/tests/misc-endpoints.ts b/server/tests/misc-endpoints.ts
index 8fab20971..b53803ee1 100644
--- a/server/tests/misc-endpoints.ts
+++ b/server/tests/misc-endpoints.ts
@@ -2,7 +2,18 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { flushTests, killallServers, makeGetRequest, runServer, ServerInfo } from './utils' 5import {
6 addVideoChannel,
7 createUser,
8 flushTests,
9 killallServers,
10 makeGetRequest,
11 runServer,
12 ServerInfo,
13 setAccessTokensToServers,
14 uploadVideo
15} from './utils'
16import { VideoPrivacy } from '../../shared/models/videos'
6 17
7const expect = chai.expect 18const expect = chai.expect
8 19
@@ -15,6 +26,7 @@ describe('Test misc endpoints', function () {
15 await flushTests() 26 await flushTests()
16 27
17 server = await runServer(1) 28 server = await runServer(1)
29 await setAccessTokensToServers([ server ])
18 }) 30 })
19 31
20 describe('Test a well known endpoints', function () { 32 describe('Test a well known endpoints', function () {
@@ -93,6 +105,64 @@ describe('Test misc endpoints', function () {
93 }) 105 })
94 }) 106 })
95 107
108 describe('Test bots endpoints', function () {
109
110 it('Should get the empty sitemap', async function () {
111 const res = await makeGetRequest({
112 url: server.url,
113 path: '/sitemap.xml',
114 statusCodeExpected: 200
115 })
116
117 expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
118 expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>')
119 })
120
121 it('Should get the empty cached sitemap', async function () {
122 const res = await makeGetRequest({
123 url: server.url,
124 path: '/sitemap.xml',
125 statusCodeExpected: 200
126 })
127
128 expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
129 expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>')
130 })
131
132 it('Should add videos, channel and accounts and get sitemap', async function () {
133 this.timeout(35000)
134
135 await uploadVideo(server.url, server.accessToken, { name: 'video 1', nsfw: false })
136 await uploadVideo(server.url, server.accessToken, { name: 'video 2', nsfw: false })
137 await uploadVideo(server.url, server.accessToken, { name: 'video 3', privacy: VideoPrivacy.PRIVATE })
138
139 await addVideoChannel(server.url, server.accessToken, { name: 'channel1', displayName: 'channel 1' })
140 await addVideoChannel(server.url, server.accessToken, { name: 'channel2', displayName: 'channel 2' })
141
142 await createUser(server.url, server.accessToken, 'user1', 'password')
143 await createUser(server.url, server.accessToken, 'user2', 'password')
144
145 const res = await makeGetRequest({
146 url: server.url,
147 path: '/sitemap.xml?t=1', // avoid using cache
148 statusCodeExpected: 200
149 })
150
151 expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
152 expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>')
153
154 expect(res.text).to.contain('<video:title><![CDATA[video 1]]></video:title>')
155 expect(res.text).to.contain('<video:title><![CDATA[video 2]]></video:title>')
156 expect(res.text).to.not.contain('<video:title><![CDATA[video 3]]></video:title>')
157
158 expect(res.text).to.contain('<url><loc>http://localhost:9001/video-channels/channel1</loc></url>')
159 expect(res.text).to.contain('<url><loc>http://localhost:9001/video-channels/channel2</loc></url>')
160
161 expect(res.text).to.contain('<url><loc>http://localhost:9001/accounts/user1</loc></url>')
162 expect(res.text).to.contain('<url><loc>http://localhost:9001/accounts/user2</loc></url>')
163 })
164 })
165
96 after(async function () { 166 after(async function () {
97 killallServers([ server ]) 167 killallServers([ server ])
98 }) 168 })