aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-09-11 16:27:07 +0200
committerChocobozzz <me@florianbigard.com>2018-09-13 14:05:49 +0200
commitc48e82b5e0478434de30626d14594a97f2402e7c (patch)
treea78e5272bd0fe4f5b41831e571e02d05f1515b82 /server/helpers
parenta651038487faa838bda3ce04695b08bc65baff70 (diff)
downloadPeerTube-c48e82b5e0478434de30626d14594a97f2402e7c.tar.gz
PeerTube-c48e82b5e0478434de30626d14594a97f2402e7c.tar.zst
PeerTube-c48e82b5e0478434de30626d14594a97f2402e7c.zip
Basic video redundancy implementation
Diffstat (limited to 'server/helpers')
-rw-r--r--server/helpers/activitypub.ts28
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts14
-rw-r--r--server/helpers/custom-validators/activitypub/cache-file.ts28
-rw-r--r--server/helpers/custom-validators/activitypub/misc.ts10
-rw-r--r--server/helpers/custom-validators/activitypub/undo.ts4
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts49
-rw-r--r--server/helpers/webtorrent.ts61
7 files changed, 137 insertions, 57 deletions
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index a9de11fb0..1304c7559 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -14,20 +14,24 @@ function activityPubContextify <T> (data: T) {
14 'https://w3id.org/security/v1', 14 'https://w3id.org/security/v1',
15 { 15 {
16 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', 16 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
17 pt: 'https://joinpeertube.org/ns',
18 schema: 'http://schema.org#',
17 Hashtag: 'as:Hashtag', 19 Hashtag: 'as:Hashtag',
18 uuid: 'http://schema.org/identifier', 20 uuid: 'schema:identifier',
19 category: 'http://schema.org/category', 21 category: 'schema:category',
20 licence: 'http://schema.org/license', 22 licence: 'schema:license',
21 subtitleLanguage: 'http://schema.org/subtitleLanguage', 23 subtitleLanguage: 'schema:subtitleLanguage',
22 sensitive: 'as:sensitive', 24 sensitive: 'as:sensitive',
23 language: 'http://schema.org/inLanguage', 25 language: 'schema:inLanguage',
24 views: 'http://schema.org/Number', 26 views: 'schema:Number',
25 stats: 'http://schema.org/Number', 27 stats: 'schema:Number',
26 size: 'http://schema.org/Number', 28 size: 'schema:Number',
27 fps: 'http://schema.org/Number', 29 fps: 'schema:Number',
28 commentsEnabled: 'http://schema.org/Boolean', 30 commentsEnabled: 'schema:Boolean',
29 waitTranscoding: 'http://schema.org/Boolean', 31 waitTranscoding: 'schema:Boolean',
30 support: 'http://schema.org/Text' 32 expires: 'schema:expires',
33 support: 'schema:Text',
34 CacheFile: 'pt:CacheFile'
31 }, 35 },
32 { 36 {
33 likes: { 37 likes: {
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index 381a29e66..2562ead9b 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -1,7 +1,10 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { Activity, ActivityType } from '../../../../shared/models/activitypub' 2import { Activity, ActivityType } from '../../../../shared/models/activitypub'
3import { 3import {
4 isActorAcceptActivityValid, isActorDeleteActivityValid, isActorFollowActivityValid, isActorRejectActivityValid, 4 isActorAcceptActivityValid,
5 isActorDeleteActivityValid,
6 isActorFollowActivityValid,
7 isActorRejectActivityValid,
5 isActorUpdateActivityValid 8 isActorUpdateActivityValid
6} from './actor' 9} from './actor'
7import { isAnnounceActivityValid } from './announce' 10import { isAnnounceActivityValid } from './announce'
@@ -11,12 +14,13 @@ import { isUndoActivityValid } from './undo'
11import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments' 14import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments'
12import { 15import {
13 isVideoFlagValid, 16 isVideoFlagValid,
14 sanitizeAndCheckVideoTorrentCreateActivity,
15 isVideoTorrentDeleteActivityValid, 17 isVideoTorrentDeleteActivityValid,
18 sanitizeAndCheckVideoTorrentCreateActivity,
16 sanitizeAndCheckVideoTorrentUpdateActivity 19 sanitizeAndCheckVideoTorrentUpdateActivity
17} from './videos' 20} from './videos'
18import { isViewActivityValid } from './view' 21import { isViewActivityValid } from './view'
19import { exists } from '../misc' 22import { exists } from '../misc'
23import { isCacheFileCreateActivityValid, isCacheFileUpdateActivityValid } from './cache-file'
20 24
21function isRootActivityValid (activity: any) { 25function isRootActivityValid (activity: any) {
22 return Array.isArray(activity['@context']) && ( 26 return Array.isArray(activity['@context']) && (
@@ -67,11 +71,13 @@ function checkCreateActivity (activity: any) {
67 isDislikeActivityValid(activity) || 71 isDislikeActivityValid(activity) ||
68 sanitizeAndCheckVideoTorrentCreateActivity(activity) || 72 sanitizeAndCheckVideoTorrentCreateActivity(activity) ||
69 isVideoFlagValid(activity) || 73 isVideoFlagValid(activity) ||
70 isVideoCommentCreateActivityValid(activity) 74 isVideoCommentCreateActivityValid(activity) ||
75 isCacheFileCreateActivityValid(activity)
71} 76}
72 77
73function checkUpdateActivity (activity: any) { 78function checkUpdateActivity (activity: any) {
74 return sanitizeAndCheckVideoTorrentUpdateActivity(activity) || 79 return isCacheFileUpdateActivityValid(activity) ||
80 sanitizeAndCheckVideoTorrentUpdateActivity(activity) ||
75 isActorUpdateActivityValid(activity) 81 isActorUpdateActivityValid(activity)
76} 82}
77 83
diff --git a/server/helpers/custom-validators/activitypub/cache-file.ts b/server/helpers/custom-validators/activitypub/cache-file.ts
new file mode 100644
index 000000000..bd70934c8
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/cache-file.ts
@@ -0,0 +1,28 @@
1import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
2import { isRemoteVideoUrlValid } from './videos'
3import { isDateValid, exists } from '../misc'
4import { CacheFileObject } from '../../../../shared/models/activitypub/objects'
5
6function isCacheFileCreateActivityValid (activity: any) {
7 return isBaseActivityValid(activity, 'Create') &&
8 isCacheFileObjectValid(activity.object)
9}
10
11function isCacheFileUpdateActivityValid (activity: any) {
12 return isBaseActivityValid(activity, 'Update') &&
13 isCacheFileObjectValid(activity.object)
14}
15
16function isCacheFileObjectValid (object: CacheFileObject) {
17 return exists(object) &&
18 object.type === 'CacheFile' &&
19 isDateValid(object.expires) &&
20 isActivityPubUrlValid(object.object) &&
21 isRemoteVideoUrlValid(object.url)
22}
23
24export {
25 isCacheFileUpdateActivityValid,
26 isCacheFileCreateActivityValid,
27 isCacheFileObjectValid
28}
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
index 6c5c7abca..4e2c57f04 100644
--- a/server/helpers/custom-validators/activitypub/misc.ts
+++ b/server/helpers/custom-validators/activitypub/misc.ts
@@ -3,7 +3,7 @@ import { CONSTRAINTS_FIELDS } from '../../../initializers'
3import { isTestInstance } from '../../core-utils' 3import { isTestInstance } from '../../core-utils'
4import { exists } from '../misc' 4import { exists } from '../misc'
5 5
6function isActivityPubUrlValid (url: string) { 6function isUrlValid (url: string) {
7 const isURLOptions = { 7 const isURLOptions = {
8 require_host: true, 8 require_host: true,
9 require_tld: true, 9 require_tld: true,
@@ -17,13 +17,18 @@ function isActivityPubUrlValid (url: string) {
17 isURLOptions.require_tld = false 17 isURLOptions.require_tld = false
18 } 18 }
19 19
20 return exists(url) && validator.isURL('' + url, isURLOptions) && validator.isLength('' + url, CONSTRAINTS_FIELDS.ACTORS.URL) 20 return exists(url) && validator.isURL('' + url, isURLOptions)
21}
22
23function isActivityPubUrlValid (url: string) {
24 return isUrlValid(url) && validator.isLength('' + url, CONSTRAINTS_FIELDS.ACTORS.URL)
21} 25}
22 26
23function isBaseActivityValid (activity: any, type: string) { 27function isBaseActivityValid (activity: any, type: string) {
24 return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && 28 return (activity['@context'] === undefined || Array.isArray(activity['@context'])) &&
25 activity.type === type && 29 activity.type === type &&
26 isActivityPubUrlValid(activity.id) && 30 isActivityPubUrlValid(activity.id) &&
31 exists(activity.actor) &&
27 (isActivityPubUrlValid(activity.actor) || isActivityPubUrlValid(activity.actor.id)) && 32 (isActivityPubUrlValid(activity.actor) || isActivityPubUrlValid(activity.actor.id)) &&
28 ( 33 (
29 activity.to === undefined || 34 activity.to === undefined ||
@@ -49,6 +54,7 @@ function setValidAttributedTo (obj: any) {
49} 54}
50 55
51export { 56export {
57 isUrlValid,
52 isActivityPubUrlValid, 58 isActivityPubUrlValid,
53 isBaseActivityValid, 59 isBaseActivityValid,
54 setValidAttributedTo 60 setValidAttributedTo
diff --git a/server/helpers/custom-validators/activitypub/undo.ts b/server/helpers/custom-validators/activitypub/undo.ts
index f50f224fa..578035893 100644
--- a/server/helpers/custom-validators/activitypub/undo.ts
+++ b/server/helpers/custom-validators/activitypub/undo.ts
@@ -2,6 +2,7 @@ import { isActorFollowActivityValid } from './actor'
2import { isBaseActivityValid } from './misc' 2import { isBaseActivityValid } from './misc'
3import { isDislikeActivityValid, isLikeActivityValid } from './rate' 3import { isDislikeActivityValid, isLikeActivityValid } from './rate'
4import { isAnnounceActivityValid } from './announce' 4import { isAnnounceActivityValid } from './announce'
5import { isCacheFileCreateActivityValid } from './cache-file'
5 6
6function isUndoActivityValid (activity: any) { 7function isUndoActivityValid (activity: any) {
7 return isBaseActivityValid(activity, 'Undo') && 8 return isBaseActivityValid(activity, 'Undo') &&
@@ -9,7 +10,8 @@ function isUndoActivityValid (activity: any) {
9 isActorFollowActivityValid(activity.object) || 10 isActorFollowActivityValid(activity.object) ||
10 isLikeActivityValid(activity.object) || 11 isLikeActivityValid(activity.object) ||
11 isDislikeActivityValid(activity.object) || 12 isDislikeActivityValid(activity.object) ||
12 isAnnounceActivityValid(activity.object) 13 isAnnounceActivityValid(activity.object) ||
14 isCacheFileCreateActivityValid(activity.object)
13 ) 15 )
14} 16}
15 17
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index 0362f43ab..f76eba474 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -75,6 +75,30 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
75 video.attributedTo.length !== 0 75 video.attributedTo.length !== 0
76} 76}
77 77
78function isRemoteVideoUrlValid (url: any) {
79 // FIXME: Old bug, we used the width to represent the resolution. Remove it in a few release (currently beta.11)
80 if (url.width && !url.height) url.height = url.width
81
82 return url.type === 'Link' &&
83 (
84 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 &&
85 isActivityPubUrlValid(url.href) &&
86 validator.isInt(url.height + '', { min: 0 }) &&
87 validator.isInt(url.size + '', { min: 0 }) &&
88 (!url.fps || validator.isInt(url.fps + '', { min: 0 }))
89 ) ||
90 (
91 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 &&
92 isActivityPubUrlValid(url.href) &&
93 validator.isInt(url.height + '', { min: 0 })
94 ) ||
95 (
96 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 &&
97 validator.isLength(url.href, { min: 5 }) &&
98 validator.isInt(url.height + '', { min: 0 })
99 )
100}
101
78// --------------------------------------------------------------------------- 102// ---------------------------------------------------------------------------
79 103
80export { 104export {
@@ -83,7 +107,8 @@ export {
83 isVideoTorrentDeleteActivityValid, 107 isVideoTorrentDeleteActivityValid,
84 isRemoteStringIdentifierValid, 108 isRemoteStringIdentifierValid,
85 isVideoFlagValid, 109 isVideoFlagValid,
86 sanitizeAndCheckVideoTorrentObject 110 sanitizeAndCheckVideoTorrentObject,
111 isRemoteVideoUrlValid
87} 112}
88 113
89// --------------------------------------------------------------------------- 114// ---------------------------------------------------------------------------
@@ -147,26 +172,4 @@ function setRemoteVideoTruncatedContent (video: any) {
147 return true 172 return true
148} 173}
149 174
150function isRemoteVideoUrlValid (url: any) {
151 // FIXME: Old bug, we used the width to represent the resolution. Remove it in a few realease (currently beta.11)
152 if (url.width && !url.height) url.height = url.width
153 175
154 return url.type === 'Link' &&
155 (
156 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 &&
157 isActivityPubUrlValid(url.href) &&
158 validator.isInt(url.height + '', { min: 0 }) &&
159 validator.isInt(url.size + '', { min: 0 }) &&
160 (!url.fps || validator.isInt(url.fps + '', { min: 0 }))
161 ) ||
162 (
163 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 &&
164 isActivityPubUrlValid(url.href) &&
165 validator.isInt(url.height + '', { min: 0 })
166 ) ||
167 (
168 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 &&
169 validator.isLength(url.href, { min: 5 }) &&
170 validator.isInt(url.height + '', { min: 0 })
171 )
172}
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts
index 1c0cc7058..2fdfd1876 100644
--- a/server/helpers/webtorrent.ts
+++ b/server/helpers/webtorrent.ts
@@ -5,44 +5,49 @@ import { createWriteStream, remove } from 'fs-extra'
5import { CONFIG } from '../initializers' 5import { CONFIG } from '../initializers'
6import { join } from 'path' 6import { join } from 'path'
7 7
8function downloadWebTorrentVideo (target: { magnetUri: string, torrentName: string }) { 8function downloadWebTorrentVideo (target: { magnetUri: string, torrentName?: string }, timeout?: number) {
9 const id = target.magnetUri || target.torrentName 9 const id = target.magnetUri || target.torrentName
10 let timer
10 11
11 const path = generateVideoTmpPath(id) 12 const path = generateVideoTmpPath(id)
12 logger.info('Importing torrent video %s', id) 13 logger.info('Importing torrent video %s', id)
13 14
14 return new Promise<string>((res, rej) => { 15 return new Promise<string>((res, rej) => {
15 const webtorrent = new WebTorrent() 16 const webtorrent = new WebTorrent()
17 let file: WebTorrent.TorrentFile
16 18
17 const torrentId = target.magnetUri || join(CONFIG.STORAGE.TORRENTS_DIR, target.torrentName) 19 const torrentId = target.magnetUri || join(CONFIG.STORAGE.TORRENTS_DIR, target.torrentName)
18 20
19 const options = { path: CONFIG.STORAGE.VIDEOS_DIR } 21 const options = { path: CONFIG.STORAGE.VIDEOS_DIR }
20 const torrent = webtorrent.add(torrentId, options, torrent => { 22 const torrent = webtorrent.add(torrentId, options, torrent => {
21 if (torrent.files.length !== 1) return rej(new Error('The number of files is not equal to 1 for ' + torrentId)) 23 if (torrent.files.length !== 1) {
24 if (timer) clearTimeout(timer)
22 25
23 const file = torrent.files[ 0 ] 26 return safeWebtorrentDestroy(webtorrent, torrentId, file.name, target.torrentName)
27 .then(() => rej(new Error('The number of files is not equal to 1 for ' + torrentId)))
28 }
29
30 file = torrent.files[ 0 ]
24 31
25 const writeStream = createWriteStream(path) 32 const writeStream = createWriteStream(path)
26 writeStream.on('finish', () => { 33 writeStream.on('finish', () => {
27 webtorrent.destroy(async err => { 34 if (timer) clearTimeout(timer)
28 if (err) return rej(err)
29
30 if (target.torrentName) {
31 remove(torrentId)
32 .catch(err => logger.error('Cannot remove torrent %s in webtorrent download.', torrentId, { err }))
33 }
34 35
35 remove(join(CONFIG.STORAGE.VIDEOS_DIR, file.name)) 36 return safeWebtorrentDestroy(webtorrent, torrentId, file.name, target.torrentName)
36 .catch(err => logger.error('Cannot remove torrent file %s in webtorrent download.', file.name, { err })) 37 .then(() => res(path))
37
38 res(path)
39 })
40 }) 38 })
41 39
42 file.createReadStream().pipe(writeStream) 40 file.createReadStream().pipe(writeStream)
43 }) 41 })
44 42
45 torrent.on('error', err => rej(err)) 43 torrent.on('error', err => rej(err))
44
45 if (timeout) {
46 timer = setTimeout(async () => {
47 return safeWebtorrentDestroy(webtorrent, torrentId, file ? file.name : undefined, target.torrentName)
48 .then(() => rej(new Error('Webtorrent download timeout.')))
49 }, timeout)
50 }
46 }) 51 })
47} 52}
48 53
@@ -51,3 +56,29 @@ function downloadWebTorrentVideo (target: { magnetUri: string, torrentName: stri
51export { 56export {
52 downloadWebTorrentVideo 57 downloadWebTorrentVideo
53} 58}
59
60// ---------------------------------------------------------------------------
61
62function safeWebtorrentDestroy (webtorrent: WebTorrent.Instance, torrentId: string, filename?: string, torrentName?: string) {
63 return new Promise(res => {
64 webtorrent.destroy(err => {
65 // Delete torrent file
66 if (torrentName) {
67 remove(torrentId)
68 .catch(err => logger.error('Cannot remove torrent %s in webtorrent download.', torrentId, { err }))
69 }
70
71 // Delete downloaded file
72 if (filename) {
73 remove(join(CONFIG.STORAGE.VIDEOS_DIR, filename))
74 .catch(err => logger.error('Cannot remove torrent file %s in webtorrent download.', filename, { err }))
75 }
76
77 if (err) {
78 logger.warn('Cannot destroy webtorrent in timeout.', { err })
79 }
80
81 return res()
82 })
83 })
84}