diff options
author | Chocobozzz <me@florianbigard.com> | 2018-09-11 16:27:07 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-09-13 14:05:49 +0200 |
commit | c48e82b5e0478434de30626d14594a97f2402e7c (patch) | |
tree | a78e5272bd0fe4f5b41831e571e02d05f1515b82 /server/helpers | |
parent | a651038487faa838bda3ce04695b08bc65baff70 (diff) | |
download | PeerTube-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.ts | 28 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/activity.ts | 14 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/cache-file.ts | 28 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/misc.ts | 10 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/undo.ts | 4 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/videos.ts | 49 | ||||
-rw-r--r-- | server/helpers/webtorrent.ts | 61 |
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 @@ | |||
1 | import * as validator from 'validator' | 1 | import * as validator from 'validator' |
2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | 2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' |
3 | import { | 3 | import { |
4 | isActorAcceptActivityValid, isActorDeleteActivityValid, isActorFollowActivityValid, isActorRejectActivityValid, | 4 | isActorAcceptActivityValid, |
5 | isActorDeleteActivityValid, | ||
6 | isActorFollowActivityValid, | ||
7 | isActorRejectActivityValid, | ||
5 | isActorUpdateActivityValid | 8 | isActorUpdateActivityValid |
6 | } from './actor' | 9 | } from './actor' |
7 | import { isAnnounceActivityValid } from './announce' | 10 | import { isAnnounceActivityValid } from './announce' |
@@ -11,12 +14,13 @@ import { isUndoActivityValid } from './undo' | |||
11 | import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments' | 14 | import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments' |
12 | import { | 15 | import { |
13 | isVideoFlagValid, | 16 | isVideoFlagValid, |
14 | sanitizeAndCheckVideoTorrentCreateActivity, | ||
15 | isVideoTorrentDeleteActivityValid, | 17 | isVideoTorrentDeleteActivityValid, |
18 | sanitizeAndCheckVideoTorrentCreateActivity, | ||
16 | sanitizeAndCheckVideoTorrentUpdateActivity | 19 | sanitizeAndCheckVideoTorrentUpdateActivity |
17 | } from './videos' | 20 | } from './videos' |
18 | import { isViewActivityValid } from './view' | 21 | import { isViewActivityValid } from './view' |
19 | import { exists } from '../misc' | 22 | import { exists } from '../misc' |
23 | import { isCacheFileCreateActivityValid, isCacheFileUpdateActivityValid } from './cache-file' | ||
20 | 24 | ||
21 | function isRootActivityValid (activity: any) { | 25 | function 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 | ||
73 | function checkUpdateActivity (activity: any) { | 78 | function 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 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | ||
2 | import { isRemoteVideoUrlValid } from './videos' | ||
3 | import { isDateValid, exists } from '../misc' | ||
4 | import { CacheFileObject } from '../../../../shared/models/activitypub/objects' | ||
5 | |||
6 | function isCacheFileCreateActivityValid (activity: any) { | ||
7 | return isBaseActivityValid(activity, 'Create') && | ||
8 | isCacheFileObjectValid(activity.object) | ||
9 | } | ||
10 | |||
11 | function isCacheFileUpdateActivityValid (activity: any) { | ||
12 | return isBaseActivityValid(activity, 'Update') && | ||
13 | isCacheFileObjectValid(activity.object) | ||
14 | } | ||
15 | |||
16 | function 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 | |||
24 | export { | ||
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' | |||
3 | import { isTestInstance } from '../../core-utils' | 3 | import { isTestInstance } from '../../core-utils' |
4 | import { exists } from '../misc' | 4 | import { exists } from '../misc' |
5 | 5 | ||
6 | function isActivityPubUrlValid (url: string) { | 6 | function 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 | |||
23 | function isActivityPubUrlValid (url: string) { | ||
24 | return isUrlValid(url) && validator.isLength('' + url, CONSTRAINTS_FIELDS.ACTORS.URL) | ||
21 | } | 25 | } |
22 | 26 | ||
23 | function isBaseActivityValid (activity: any, type: string) { | 27 | function 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 | ||
51 | export { | 56 | export { |
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' | |||
2 | import { isBaseActivityValid } from './misc' | 2 | import { isBaseActivityValid } from './misc' |
3 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | 3 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' |
4 | import { isAnnounceActivityValid } from './announce' | 4 | import { isAnnounceActivityValid } from './announce' |
5 | import { isCacheFileCreateActivityValid } from './cache-file' | ||
5 | 6 | ||
6 | function isUndoActivityValid (activity: any) { | 7 | function 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 | ||
78 | function 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 | ||
80 | export { | 104 | export { |
@@ -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 | ||
150 | function 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' | |||
5 | import { CONFIG } from '../initializers' | 5 | import { CONFIG } from '../initializers' |
6 | import { join } from 'path' | 6 | import { join } from 'path' |
7 | 7 | ||
8 | function downloadWebTorrentVideo (target: { magnetUri: string, torrentName: string }) { | 8 | function 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 | |||
51 | export { | 56 | export { |
52 | downloadWebTorrentVideo | 57 | downloadWebTorrentVideo |
53 | } | 58 | } |
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | function 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 | } | ||