aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/initializers
diff options
context:
space:
mode:
Diffstat (limited to 'server/initializers')
-rw-r--r--server/initializers/checker-after-init.ts325
-rw-r--r--server/initializers/checker-before-init.ts162
-rw-r--r--server/initializers/config.ts688
-rw-r--r--server/initializers/constants.ts1396
-rw-r--r--server/initializers/database.ts233
-rw-r--r--server/initializers/installer.ts199
-rw-r--r--server/initializers/migrations/0505-user-last-login-date.ts26
-rw-r--r--server/initializers/migrations/0510-video-file-metadata.ts38
-rw-r--r--server/initializers/migrations/0515-video-abuse-reason-timestamps.ts31
-rw-r--r--server/initializers/migrations/0520-abuses-split.ts83
-rw-r--r--server/initializers/migrations/0525-abuse-messages.ts54
-rw-r--r--server/initializers/migrations/0530-playlist-multiple-video.ts46
-rw-r--r--server/initializers/migrations/0535-video-live.ts39
-rw-r--r--server/initializers/migrations/0540-video-file-infohash.ts26
-rw-r--r--server/initializers/migrations/0545-video-live-save-replay.ts26
-rw-r--r--server/initializers/migrations/0550-actor-follow-cleanup.ts27
-rw-r--r--server/initializers/migrations/0555-actor-follow-url.ts26
-rw-r--r--server/initializers/migrations/0560-user-feed-token.ts51
-rw-r--r--server/initializers/migrations/0565-actor-follow-local-url.ts27
-rw-r--r--server/initializers/migrations/0570-permanent-live.ts27
-rw-r--r--server/initializers/migrations/0575-duplicate-thumbnail.ts24
-rw-r--r--server/initializers/migrations/0580-caption-filename.ts48
-rw-r--r--server/initializers/migrations/0585-video-file-names.ts55
-rw-r--r--server/initializers/migrations/0590-trackers.ts44
-rw-r--r--server/initializers/migrations/0595-remote-url.ts130
-rw-r--r--server/initializers/migrations/0600-duplicate-video-files.ts33
-rw-r--r--server/initializers/migrations/0605-actor-missing-keys.ts33
-rw-r--r--server/initializers/migrations/0610-views-index copy.ts20
-rw-r--r--server/initializers/migrations/0612-captions-unique.ts23
-rw-r--r--server/initializers/migrations/0615-latest-versions-notification-settings.ts44
-rw-r--r--server/initializers/migrations/0620-latest-versions-application.ts27
-rw-r--r--server/initializers/migrations/0625-latest-versions-notification.ts26
-rw-r--r--server/initializers/migrations/0630-banner.ts50
-rw-r--r--server/initializers/migrations/0635-actor-image-size.ts35
-rw-r--r--server/initializers/migrations/0640-unique-keys.ts39
-rw-r--r--server/initializers/migrations/0645-actor-remote-creation-date.ts26
-rw-r--r--server/initializers/migrations/0650-actor-custom-pages.ts33
-rw-r--r--server/initializers/migrations/0655-streaming-playlist-filenames.ts66
-rw-r--r--server/initializers/migrations/0660-object-storage.ts56
-rw-r--r--server/initializers/migrations/0665-no-account-warning-modal.ts27
-rw-r--r--server/initializers/migrations/0670-pending-job-default.ts27
-rw-r--r--server/initializers/migrations/0675-p2p-enabled.ts21
-rw-r--r--server/initializers/migrations/0680-files-storage-default.ts20
-rw-r--r--server/initializers/migrations/0685-multiple-actor-images.ts62
-rw-r--r--server/initializers/migrations/0690-live-latency-mode.ts35
-rw-r--r--server/initializers/migrations/0695-remove-remote-rates.ts28
-rw-r--r--server/initializers/migrations/0700-edition-finished-notification.ts42
-rw-r--r--server/initializers/migrations/0705-local-video-viewers.ts52
-rw-r--r--server/initializers/migrations/0710-live-sessions.ts34
-rw-r--r--server/initializers/migrations/0715-video-source.ts34
-rw-r--r--server/initializers/migrations/0720-session-ending-processed.ts56
-rw-r--r--server/initializers/migrations/0725-node-version.ts66
-rw-r--r--server/initializers/migrations/0730-video-channel-sync.ts36
-rw-r--r--server/initializers/migrations/0735-video-channel-sync-import-foreign-key.ts32
-rw-r--r--server/initializers/migrations/0740-fix-old-enums.ts33
-rw-r--r--server/initializers/migrations/0745-user-otp.ts29
-rw-r--r--server/initializers/migrations/0750-user-registration.ts58
-rw-r--r--server/initializers/migrations/0755-unique-viewer-url.ts27
-rw-r--r--server/initializers/migrations/0760-video-live-replay-setting.ts125
-rw-r--r--server/initializers/migrations/0765-remote-transcoding.ts78
-rw-r--r--server/initializers/migrations/0770-actor-preferred-username.ts44
-rw-r--r--server/initializers/migrations/0775-add-user-is-email-public.ts25
-rw-r--r--server/initializers/migrations/0780-notification-registration.ts30
-rw-r--r--server/initializers/migrations/0785-video-password-protection.ts31
-rw-r--r--server/initializers/migrations/0790-thumbnail-disk.ts47
-rw-r--r--server/initializers/migrations/0795-duplicate-runner-name.ts24
-rw-r--r--server/initializers/migrations/0800-video-replace-file.ts38
-rw-r--r--server/initializers/migrator.ts105
68 files changed, 0 insertions, 5608 deletions
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
deleted file mode 100644
index 4281e2bb5..000000000
--- a/server/initializers/checker-after-init.ts
+++ /dev/null
@@ -1,325 +0,0 @@
1import config from 'config'
2import { readFileSync, writeFileSync } from 'fs-extra'
3import { URL } from 'url'
4import { uniqify } from '@shared/core-utils'
5import { getFFmpegVersion } from '@shared/ffmpeg'
6import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
7import { RecentlyAddedStrategy } from '../../shared/models/redundancy'
8import { isProdInstance, parseBytes, parseSemVersion } from '../helpers/core-utils'
9import { isArray } from '../helpers/custom-validators/misc'
10import { logger } from '../helpers/logger'
11import { ApplicationModel, getServerActor } from '../models/application/application'
12import { OAuthClientModel } from '../models/oauth/oauth-client'
13import { UserModel } from '../models/user/user'
14import { CONFIG, getLocalConfigFilePath, isEmailEnabled, reloadConfig } from './config'
15import { WEBSERVER } from './constants'
16
17async function checkActivityPubUrls () {
18 const actor = await getServerActor()
19
20 const parsed = new URL(actor.url)
21 if (WEBSERVER.HOST !== parsed.host) {
22 const NODE_ENV = config.util.getEnv('NODE_ENV')
23 const NODE_CONFIG_DIR = config.util.getEnv('NODE_CONFIG_DIR')
24
25 logger.warn(
26 'It seems PeerTube was started (and created some data) with another domain name. ' +
27 'This means you will not be able to federate! ' +
28 'Please use %s %s npm run update-host to fix this.',
29 NODE_CONFIG_DIR ? `NODE_CONFIG_DIR=${NODE_CONFIG_DIR}` : '',
30 NODE_ENV ? `NODE_ENV=${NODE_ENV}` : ''
31 )
32 }
33}
34
35// Some checks on configuration files or throw if there is an error
36function checkConfig () {
37
38 const configFiles = config.util.getConfigSources().map(s => s.name).join(' -> ')
39 logger.info('Using following configuration file hierarchy: %s.', configFiles)
40
41 checkRemovedConfigKeys()
42
43 checkSecretsConfig()
44 checkEmailConfig()
45 checkNSFWPolicyConfig()
46 checkLocalRedundancyConfig()
47 checkRemoteRedundancyConfig()
48 checkStorageConfig()
49 checkTranscodingConfig()
50 checkImportConfig()
51 checkBroadcastMessageConfig()
52 checkSearchConfig()
53 checkLiveConfig()
54 checkObjectStorageConfig()
55 checkVideoStudioConfig()
56}
57
58// We get db by param to not import it in this file (import orders)
59async function clientsExist () {
60 const totalClients = await OAuthClientModel.countTotal()
61
62 return totalClients !== 0
63}
64
65// We get db by param to not import it in this file (import orders)
66async function usersExist () {
67 const totalUsers = await UserModel.countTotal()
68
69 return totalUsers !== 0
70}
71
72// We get db by param to not import it in this file (import orders)
73async function applicationExist () {
74 const totalApplication = await ApplicationModel.countTotal()
75
76 return totalApplication !== 0
77}
78
79async function checkFFmpegVersion () {
80 const version = await getFFmpegVersion()
81 const { major, minor, patch } = parseSemVersion(version)
82
83 if (major < 4 || (major === 4 && minor < 1)) {
84 logger.warn('Your ffmpeg version (%s) is outdated. PeerTube supports ffmpeg >= 4.1. Please upgrade ffmpeg.', version)
85 }
86
87 if (major === 4 && minor === 4 && patch === 0) {
88 logger.warn('There is a bug in ffmpeg 4.4.0 with HLS videos. Please upgrade ffmpeg.')
89 }
90}
91
92// ---------------------------------------------------------------------------
93
94export {
95 checkConfig,
96 clientsExist,
97 checkFFmpegVersion,
98 usersExist,
99 applicationExist,
100 checkActivityPubUrls
101}
102
103// ---------------------------------------------------------------------------
104
105function checkRemovedConfigKeys () {
106 // Moved configuration keys
107 if (config.has('services.csp-logger')) {
108 logger.warn('services.csp-logger configuration has been renamed to csp.report_uri. Please update your configuration file.')
109 }
110
111 if (config.has('transcoding.webtorrent.enabled')) {
112 const localConfigPath = getLocalConfigFilePath()
113
114 const content = readFileSync(localConfigPath, { encoding: 'utf-8' })
115 if (!content.includes('"webtorrent"')) {
116 throw new Error('Please rename transcoding.webtorrent.enabled key to transcoding.web_videos.enabled in your configuration file')
117 }
118
119 try {
120 logger.info(
121 'Replacing "transcoding.webtorrent.enabled" key to "transcoding.web_videos.enabled" in your local configuration ' + localConfigPath
122 )
123
124 writeFileSync(localConfigPath, content.replace('"webtorrent"', '"web_videos"'), { encoding: 'utf-8' })
125
126 reloadConfig()
127 } catch (err) {
128 logger.error('Cannot write new configuration to file ' + localConfigPath, { err })
129 }
130 }
131}
132
133function checkSecretsConfig () {
134 if (!CONFIG.SECRETS.PEERTUBE) {
135 throw new Error('secrets.peertube is missing in config. Generate one using `openssl rand -hex 32`')
136 }
137}
138
139function checkEmailConfig () {
140 if (!isEmailEnabled()) {
141 if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
142 throw new Error('SMTP is not configured but you require signup email verification.')
143 }
144
145 if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_APPROVAL) {
146 // eslint-disable-next-line max-len
147 logger.warn('SMTP is not configured but signup approval is enabled: PeerTube will not be able to send an email to the user upon acceptance/rejection of the registration request')
148 }
149
150 if (CONFIG.CONTACT_FORM.ENABLED) {
151 logger.warn('SMTP is not configured so the contact form will not work.')
152 }
153 }
154}
155
156function checkNSFWPolicyConfig () {
157 const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
158
159 const available = [ 'do_not_list', 'blur', 'display' ]
160 if (available.includes(defaultNSFWPolicy) === false) {
161 throw new Error('NSFW policy setting should be ' + available.join(' or ') + ' instead of ' + defaultNSFWPolicy)
162 }
163}
164
165function checkLocalRedundancyConfig () {
166 const redundancyVideos = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES
167
168 if (isArray(redundancyVideos)) {
169 const available = [ 'most-views', 'trending', 'recently-added' ]
170
171 for (const r of redundancyVideos) {
172 if (available.includes(r.strategy) === false) {
173 throw new Error('Videos redundancy should have ' + available.join(' or ') + ' strategy instead of ' + r.strategy)
174 }
175
176 // Lifetime should not be < 10 hours
177 if (isProdInstance() && r.minLifetime < 1000 * 3600 * 10) {
178 throw new Error('Video redundancy minimum lifetime should be >= 10 hours for strategy ' + r.strategy)
179 }
180 }
181
182 const filtered = uniqify(redundancyVideos.map(r => r.strategy))
183 if (filtered.length !== redundancyVideos.length) {
184 throw new Error('Redundancy video entries should have unique strategies')
185 }
186
187 const recentlyAddedStrategy = redundancyVideos.find(r => r.strategy === 'recently-added') as RecentlyAddedStrategy
188 if (recentlyAddedStrategy && isNaN(recentlyAddedStrategy.minViews)) {
189 throw new Error('Min views in recently added strategy is not a number')
190 }
191 } else {
192 throw new Error('Videos redundancy should be an array (you must uncomment lines containing - too)')
193 }
194}
195
196function checkRemoteRedundancyConfig () {
197 const acceptFrom = CONFIG.REMOTE_REDUNDANCY.VIDEOS.ACCEPT_FROM
198 const acceptFromValues = new Set<VideoRedundancyConfigFilter>([ 'nobody', 'anybody', 'followings' ])
199
200 if (acceptFromValues.has(acceptFrom) === false) {
201 throw new Error('remote_redundancy.videos.accept_from has an incorrect value')
202 }
203}
204
205function checkStorageConfig () {
206 // Check storage directory locations
207 if (isProdInstance()) {
208 const configStorage = config.get<{ [ name: string ]: string }>('storage')
209
210 for (const key of Object.keys(configStorage)) {
211 if (configStorage[key].startsWith('storage/')) {
212 logger.warn(
213 'Directory of %s should not be in the production directory of PeerTube. Please check your production configuration file.',
214 key
215 )
216 }
217 }
218 }
219
220 if (CONFIG.STORAGE.WEB_VIDEOS_DIR === CONFIG.STORAGE.REDUNDANCY_DIR) {
221 logger.warn('Redundancy directory should be different than the videos folder.')
222 }
223}
224
225function checkTranscodingConfig () {
226 if (CONFIG.TRANSCODING.ENABLED) {
227 if (CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) {
228 throw new Error('You need to enable at least Web Video transcoding or HLS transcoding.')
229 }
230
231 if (CONFIG.TRANSCODING.CONCURRENCY <= 0) {
232 throw new Error('Transcoding concurrency should be > 0')
233 }
234 }
235
236 if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED || CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED) {
237 if (CONFIG.IMPORT.VIDEOS.CONCURRENCY <= 0) {
238 throw new Error('Video import concurrency should be > 0')
239 }
240 }
241}
242
243function checkImportConfig () {
244 if (CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED && !CONFIG.IMPORT.VIDEOS.HTTP) {
245 throw new Error('You need to enable HTTP import to allow synchronization')
246 }
247}
248
249function checkBroadcastMessageConfig () {
250 if (CONFIG.BROADCAST_MESSAGE.ENABLED) {
251 const currentLevel = CONFIG.BROADCAST_MESSAGE.LEVEL
252 const available = [ 'info', 'warning', 'error' ]
253
254 if (available.includes(currentLevel) === false) {
255 throw new Error('Broadcast message level should be ' + available.join(' or ') + ' instead of ' + currentLevel)
256 }
257 }
258}
259
260function checkSearchConfig () {
261 if (CONFIG.SEARCH.SEARCH_INDEX.ENABLED === true) {
262 if (CONFIG.SEARCH.REMOTE_URI.USERS === false) {
263 throw new Error('You cannot enable search index without enabling remote URI search for users.')
264 }
265 }
266}
267
268function checkLiveConfig () {
269 if (CONFIG.LIVE.ENABLED === true) {
270 if (CONFIG.LIVE.ALLOW_REPLAY === true && CONFIG.TRANSCODING.ENABLED === false) {
271 throw new Error('Live allow replay cannot be enabled if transcoding is not enabled.')
272 }
273
274 if (CONFIG.LIVE.RTMP.ENABLED === false && CONFIG.LIVE.RTMPS.ENABLED === false) {
275 throw new Error('You must enable at least RTMP or RTMPS')
276 }
277
278 if (CONFIG.LIVE.RTMPS.ENABLED) {
279 if (!CONFIG.LIVE.RTMPS.KEY_FILE) {
280 throw new Error('You must specify a key file to enable RTMPS')
281 }
282
283 if (!CONFIG.LIVE.RTMPS.CERT_FILE) {
284 throw new Error('You must specify a cert file to enable RTMPS')
285 }
286 }
287 }
288}
289
290function checkObjectStorageConfig () {
291 if (CONFIG.OBJECT_STORAGE.ENABLED === true) {
292
293 if (!CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME) {
294 throw new Error('videos_bucket should be set when object storage support is enabled.')
295 }
296
297 if (!CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME) {
298 throw new Error('streaming_playlists_bucket should be set when object storage support is enabled.')
299 }
300
301 if (
302 CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME &&
303 CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.PREFIX
304 ) {
305 if (CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === '') {
306 throw new Error('Object storage bucket prefixes should be set when the same bucket is used for both types of video.')
307 }
308
309 throw new Error(
310 'Object storage bucket prefixes should be set to different values when the same bucket is used for both types of video.'
311 )
312 }
313
314 if (CONFIG.OBJECT_STORAGE.MAX_UPLOAD_PART > parseBytes('250MB')) {
315 // eslint-disable-next-line max-len
316 logger.warn(`Object storage max upload part seems to have a big value (${CONFIG.OBJECT_STORAGE.MAX_UPLOAD_PART} bytes). Consider using a lower one (like 100MB).`)
317 }
318 }
319}
320
321function checkVideoStudioConfig () {
322 if (CONFIG.VIDEO_STUDIO.ENABLED === true && CONFIG.TRANSCODING.ENABLED === false) {
323 throw new Error('Video studio cannot be enabled if transcoding is disabled')
324 }
325}
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
deleted file mode 100644
index 0139ded4f..000000000
--- a/server/initializers/checker-before-init.ts
+++ /dev/null
@@ -1,162 +0,0 @@
1import { IConfig } from 'config'
2import { promisify0 } from '@shared/core-utils'
3import { parseSemVersion } from '../helpers/core-utils'
4import { logger } from '../helpers/logger'
5
6// Special behaviour for config because we can reload it
7const config: IConfig = require('config')
8
9// ONLY USE CORE MODULES IN THIS FILE!
10
11// Check the config files
12function checkMissedConfig () {
13 const required = [ 'listen.port', 'listen.hostname',
14 'webserver.https', 'webserver.hostname', 'webserver.port',
15 'secrets.peertube',
16 'trust_proxy',
17 'oauth2.token_lifetime.access_token', 'oauth2.token_lifetime.refresh_token',
18 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max',
19 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',
20 'email.body.signature', 'email.subject.prefix',
21 'storage.avatars', 'storage.web_videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache',
22 'storage.redundancy', 'storage.tmp', 'storage.streaming_playlists', 'storage.plugins', 'storage.well_known',
23 'log.level', 'log.rotation.enabled', 'log.rotation.max_file_size', 'log.rotation.max_files', 'log.anonymize_ip',
24 'log.log_ping_requests', 'log.log_tracker_unknown_infohash', 'log.prettify_sql', 'log.accept_client_log',
25 'open_telemetry.metrics.enabled', 'open_telemetry.metrics.prometheus_exporter.hostname',
26 'open_telemetry.metrics.prometheus_exporter.port', 'open_telemetry.tracing.enabled', 'open_telemetry.tracing.jaeger_exporter.endpoint',
27 'open_telemetry.metrics.http_request_duration.enabled',
28 'user.history.videos.enabled', 'user.video_quota', 'user.video_quota_daily',
29 'video_channels.max_per_user',
30 'csp.enabled', 'csp.report_only', 'csp.report_uri',
31 'security.frameguard.enabled', 'security.powered_by_header.enabled',
32 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'cache.storyboards.size',
33 'admin.email', 'contact_form.enabled',
34 'signup.enabled', 'signup.limit', 'signup.requires_approval', 'signup.requires_email_verification', 'signup.minimum_age',
35 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
36 'redundancy.videos.strategies', 'redundancy.videos.check_interval',
37 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.web_videos.enabled',
38 'transcoding.hls.enabled', 'transcoding.profile', 'transcoding.concurrency',
39 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p',
40 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p',
41 'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'transcoding.remote_runners.enabled',
42 'video_studio.enabled', 'video_studio.remote_runners.enabled',
43 'video_file.update.enabled',
44 'remote_runners.stalled_jobs.vod', 'remote_runners.stalled_jobs.live',
45 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'import.videos.timeout',
46 'import.video_channel_synchronization.enabled', 'import.video_channel_synchronization.max_per_user',
47 'import.video_channel_synchronization.check_interval', 'import.video_channel_synchronization.videos_limit_per_synchronization',
48 'import.video_channel_synchronization.full_sync_videos_limit',
49 'auto_blacklist.videos.of_users.enabled', 'trending.videos.interval_days',
50 'client.videos.miniature.display_author_avatar',
51 'client.videos.miniature.prefer_author_display_name', 'client.menu.login.redirect_on_single_external_auth',
52 'defaults.publish.download_enabled', 'defaults.publish.comments_enabled', 'defaults.publish.privacy', 'defaults.publish.licence',
53 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
54 'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
55 'services.twitter.username', 'services.twitter.whitelisted',
56 'followers.instance.enabled', 'followers.instance.manual_approval',
57 'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces',
58 'history.videos.max_age', 'views.videos.remote.max_age', 'views.videos.local_buffer_update_interval', 'views.videos.ip_view_expiration',
59 'rates_limit.api.window', 'rates_limit.api.max', 'rates_limit.login.window', 'rates_limit.login.max',
60 'rates_limit.signup.window', 'rates_limit.signup.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max',
61 'rates_limit.receive_client_log.window', 'rates_limit.receive_client_log.max', 'rates_limit.plugins.window', 'rates_limit.plugins.max',
62 'rates_limit.well_known.window', 'rates_limit.well_known.max', 'rates_limit.feeds.window', 'rates_limit.feeds.max',
63 'rates_limit.activity_pub.window', 'rates_limit.activity_pub.max', 'rates_limit.client.window', 'rates_limit.client.max',
64 'static_files.private_files_require_auth',
65 'object_storage.enabled', 'object_storage.endpoint', 'object_storage.region', 'object_storage.upload_acl.public',
66 'object_storage.upload_acl.private', 'object_storage.proxy.proxify_private_files', 'object_storage.credentials.access_key_id',
67 'object_storage.credentials.secret_access_key', 'object_storage.max_upload_part', 'object_storage.streaming_playlists.bucket_name',
68 'object_storage.streaming_playlists.prefix', 'object_storage.streaming_playlists.base_url', 'object_storage.web_videos.bucket_name',
69 'object_storage.web_videos.prefix', 'object_storage.web_videos.base_url',
70 'theme.default',
71 'feeds.videos.count', 'feeds.comments.count',
72 'geo_ip.enabled', 'geo_ip.country.database_url',
73 'remote_redundancy.videos.accept_from',
74 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions',
75 'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url',
76 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url',
77 'search.search_index.disable_local_search', 'search.search_index.is_default_search',
78 'live.enabled', 'live.allow_replay', 'live.latency_setting.enabled', 'live.max_duration',
79 'live.max_user_lives', 'live.max_instance_lives',
80 'live.rtmp.enabled', 'live.rtmp.port', 'live.rtmp.hostname', 'live.rtmp.public_hostname',
81 'live.rtmps.enabled', 'live.rtmps.port', 'live.rtmps.hostname', 'live.rtmps.public_hostname',
82 'live.rtmps.key_file', 'live.rtmps.cert_file',
83 'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile',
84 'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p',
85 'live.transcoding.resolutions.480p', 'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p',
86 'live.transcoding.resolutions.1440p', 'live.transcoding.resolutions.2160p', 'live.transcoding.always_transcode_original_resolution',
87 'live.transcoding.remote_runners.enabled'
88 ]
89
90 const requiredAlternatives = [
91 [ // set
92 [ 'redis.hostname', 'redis.port' ], // alternative
93 [ 'redis.socket' ],
94 [ 'redis.sentinel.master_name', 'redis.sentinel.sentinels[0].hostname', 'redis.sentinel.sentinels[0].port' ]
95 ]
96 ]
97 const miss: string[] = []
98
99 for (const key of required) {
100 if (!config.has(key)) {
101 miss.push(key)
102 }
103 }
104
105 const redundancyVideos = config.get<any>('redundancy.videos.strategies')
106
107 if (Array.isArray(redundancyVideos)) {
108 for (const r of redundancyVideos) {
109 if (!r.size) miss.push('redundancy.videos.strategies.size')
110 if (!r.min_lifetime) miss.push('redundancy.videos.strategies.min_lifetime')
111 }
112 }
113
114 const missingAlternatives = requiredAlternatives.filter(
115 set => !set.find(alternative => !alternative.find(key => !config.has(key)))
116 )
117
118 missingAlternatives
119 .forEach(set => set[0].forEach(key => miss.push(key)))
120
121 return miss
122}
123
124// Check the available codecs
125// We get CONFIG by param to not import it in this file (import orders)
126async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) {
127 if (CONFIG.TRANSCODING.ENABLED === false) return undefined
128
129 const Ffmpeg = require('fluent-ffmpeg')
130 const getAvailableCodecsPromise = promisify0(Ffmpeg.getAvailableCodecs)
131 const codecs = await getAvailableCodecsPromise()
132 const canEncode = [ 'libx264' ]
133
134 for (const codec of canEncode) {
135 if (codecs[codec] === undefined) {
136 throw new Error('Unknown codec ' + codec + ' in FFmpeg.')
137 }
138
139 if (codecs[codec].canEncode !== true) {
140 throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg')
141 }
142 }
143}
144
145function checkNodeVersion () {
146 const v = process.version
147 const { major } = parseSemVersion(v)
148
149 logger.debug('Checking NodeJS version %s.', v)
150
151 if (major <= 12) {
152 throw new Error('Your NodeJS version ' + v + ' is not supported. Please upgrade.')
153 }
154}
155
156// ---------------------------------------------------------------------------
157
158export {
159 checkFFmpeg,
160 checkMissedConfig,
161 checkNodeVersion
162}
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
deleted file mode 100644
index 3e3b8ad1f..000000000
--- a/server/initializers/config.ts
+++ /dev/null
@@ -1,688 +0,0 @@
1import bytes from 'bytes'
2import { IConfig } from 'config'
3import { dirname, join } from 'path'
4import { decacheModule } from '@server/helpers/decache'
5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
6import { BroadcastMessageLevel } from '@shared/models/server'
7import { buildPath, root } from '../../shared/core-utils'
8import { VideoPrivacy, VideosRedundancyStrategy } from '../../shared/models'
9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
10import { parseBytes, parseDurationToMs } from '../helpers/core-utils'
11
12// Use a variable to reload the configuration if we need
13let config: IConfig = require('config')
14
15const configChangedHandlers: Function[] = []
16
17const CONFIG = {
18 CUSTOM_FILE: getLocalConfigFilePath(),
19 LISTEN: {
20 PORT: config.get<number>('listen.port'),
21 HOSTNAME: config.get<string>('listen.hostname')
22 },
23 SECRETS: {
24 PEERTUBE: config.get<string>('secrets.peertube')
25 },
26 DATABASE: {
27 DBNAME: config.has('database.name') ? config.get<string>('database.name') : 'peertube' + config.get<string>('database.suffix'),
28 HOSTNAME: config.get<string>('database.hostname'),
29 PORT: config.get<number>('database.port'),
30 SSL: config.get<boolean>('database.ssl'),
31 USERNAME: config.get<string>('database.username'),
32 PASSWORD: config.get<string>('database.password'),
33 POOL: {
34 MAX: config.get<number>('database.pool.max')
35 }
36 },
37 REDIS: {
38 HOSTNAME: config.has('redis.hostname') ? config.get<string>('redis.hostname') : null,
39 PORT: config.has('redis.port') ? config.get<number>('redis.port') : null,
40 SOCKET: config.has('redis.socket') ? config.get<string>('redis.socket') : null,
41 AUTH: config.has('redis.auth') ? config.get<string>('redis.auth') : null,
42 DB: config.has('redis.db') ? config.get<number>('redis.db') : null,
43 SENTINEL: {
44 ENABLED: config.has('redis.sentinel.enabled') ? config.get<boolean>('redis.sentinel.enabled') : false,
45 ENABLE_TLS: config.has('redis.sentinel.enable_tls') ? config.get<boolean>('redis.sentinel.enable_tls') : false,
46 SENTINELS: config.has('redis.sentinel.sentinels') ? config.get<{ hostname: string, port: number }[]>('redis.sentinel.sentinels') : [],
47 MASTER_NAME: config.has('redis.sentinel.master_name') ? config.get<string>('redis.sentinel.master_name') : null
48 }
49 },
50 SMTP: {
51 TRANSPORT: config.has('smtp.transport') ? config.get<string>('smtp.transport') : 'smtp',
52 SENDMAIL: config.has('smtp.sendmail') ? config.get<string>('smtp.sendmail') : null,
53 HOSTNAME: config.get<string>('smtp.hostname'),
54 PORT: config.get<number>('smtp.port'),
55 USERNAME: config.get<string>('smtp.username'),
56 PASSWORD: config.get<string>('smtp.password'),
57 TLS: config.get<boolean>('smtp.tls'),
58 DISABLE_STARTTLS: config.get<boolean>('smtp.disable_starttls'),
59 CA_FILE: config.get<string>('smtp.ca_file'),
60 FROM_ADDRESS: config.get<string>('smtp.from_address')
61 },
62 EMAIL: {
63 BODY: {
64 SIGNATURE: config.get<string>('email.body.signature')
65 },
66 SUBJECT: {
67 PREFIX: config.get<string>('email.subject.prefix') + ' '
68 }
69 },
70
71 CLIENT: {
72 VIDEOS: {
73 MINIATURE: {
74 get PREFER_AUTHOR_DISPLAY_NAME () { return config.get<boolean>('client.videos.miniature.prefer_author_display_name') },
75 get DISPLAY_AUTHOR_AVATAR () { return config.get<boolean>('client.videos.miniature.display_author_avatar') }
76 },
77 RESUMABLE_UPLOAD: {
78 get MAX_CHUNK_SIZE () { return parseBytes(config.get<number>('client.videos.resumable_upload.max_chunk_size') || 0) }
79 }
80 },
81 MENU: {
82 LOGIN: {
83 get REDIRECT_ON_SINGLE_EXTERNAL_AUTH () { return config.get<boolean>('client.menu.login.redirect_on_single_external_auth') }
84 }
85 }
86 },
87
88 DEFAULTS: {
89 PUBLISH: {
90 DOWNLOAD_ENABLED: config.get<boolean>('defaults.publish.download_enabled'),
91 COMMENTS_ENABLED: config.get<boolean>('defaults.publish.comments_enabled'),
92 PRIVACY: config.get<VideoPrivacy>('defaults.publish.privacy'),
93 LICENCE: config.get<number>('defaults.publish.licence')
94 },
95 P2P: {
96 WEBAPP: {
97 ENABLED: config.get<boolean>('defaults.p2p.webapp.enabled')
98 },
99 EMBED: {
100 ENABLED: config.get<boolean>('defaults.p2p.embed.enabled')
101 }
102 }
103 },
104
105 STORAGE: {
106 TMP_DIR: buildPath(config.get<string>('storage.tmp')),
107 TMP_PERSISTENT_DIR: buildPath(config.get<string>('storage.tmp_persistent')),
108 BIN_DIR: buildPath(config.get<string>('storage.bin')),
109 ACTOR_IMAGES_DIR: buildPath(config.get<string>('storage.avatars')),
110 LOG_DIR: buildPath(config.get<string>('storage.logs')),
111 WEB_VIDEOS_DIR: buildPath(config.get<string>('storage.web_videos')),
112 STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')),
113 REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')),
114 THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')),
115 STORYBOARDS_DIR: buildPath(config.get<string>('storage.storyboards')),
116 PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')),
117 CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')),
118 TORRENTS_DIR: buildPath(config.get<string>('storage.torrents')),
119 CACHE_DIR: buildPath(config.get<string>('storage.cache')),
120 PLUGINS_DIR: buildPath(config.get<string>('storage.plugins')),
121 CLIENT_OVERRIDES_DIR: buildPath(config.get<string>('storage.client_overrides')),
122 WELL_KNOWN_DIR: buildPath(config.get<string>('storage.well_known'))
123 },
124 STATIC_FILES: {
125 PRIVATE_FILES_REQUIRE_AUTH: config.get<boolean>('static_files.private_files_require_auth')
126 },
127 OBJECT_STORAGE: {
128 ENABLED: config.get<boolean>('object_storage.enabled'),
129 MAX_UPLOAD_PART: bytes.parse(config.get<string>('object_storage.max_upload_part')),
130 ENDPOINT: config.get<string>('object_storage.endpoint'),
131 REGION: config.get<string>('object_storage.region'),
132 UPLOAD_ACL: {
133 PUBLIC: config.get<string>('object_storage.upload_acl.public'),
134 PRIVATE: config.get<string>('object_storage.upload_acl.private')
135 },
136 CREDENTIALS: {
137 ACCESS_KEY_ID: config.get<string>('object_storage.credentials.access_key_id'),
138 SECRET_ACCESS_KEY: config.get<string>('object_storage.credentials.secret_access_key')
139 },
140 PROXY: {
141 PROXIFY_PRIVATE_FILES: config.get<boolean>('object_storage.proxy.proxify_private_files')
142 },
143 WEB_VIDEOS: {
144 BUCKET_NAME: config.get<string>('object_storage.web_videos.bucket_name'),
145 PREFIX: config.get<string>('object_storage.web_videos.prefix'),
146 BASE_URL: config.get<string>('object_storage.web_videos.base_url')
147 },
148 STREAMING_PLAYLISTS: {
149 BUCKET_NAME: config.get<string>('object_storage.streaming_playlists.bucket_name'),
150 PREFIX: config.get<string>('object_storage.streaming_playlists.prefix'),
151 BASE_URL: config.get<string>('object_storage.streaming_playlists.base_url')
152 }
153 },
154 WEBSERVER: {
155 SCHEME: config.get<boolean>('webserver.https') === true ? 'https' : 'http',
156 WS: config.get<boolean>('webserver.https') === true ? 'wss' : 'ws',
157 HOSTNAME: config.get<string>('webserver.hostname'),
158 PORT: config.get<number>('webserver.port')
159 },
160 OAUTH2: {
161 TOKEN_LIFETIME: {
162 ACCESS_TOKEN: parseDurationToMs(config.get<string>('oauth2.token_lifetime.access_token')),
163 REFRESH_TOKEN: parseDurationToMs(config.get<string>('oauth2.token_lifetime.refresh_token'))
164 }
165 },
166 RATES_LIMIT: {
167 API: {
168 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')),
169 MAX: config.get<number>('rates_limit.api.max')
170 },
171 SIGNUP: {
172 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.signup.window')),
173 MAX: config.get<number>('rates_limit.signup.max')
174 },
175 LOGIN: {
176 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.login.window')),
177 MAX: config.get<number>('rates_limit.login.max')
178 },
179 RECEIVE_CLIENT_LOG: {
180 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.receive_client_log.window')),
181 MAX: config.get<number>('rates_limit.receive_client_log.max')
182 },
183 ASK_SEND_EMAIL: {
184 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.ask_send_email.window')),
185 MAX: config.get<number>('rates_limit.ask_send_email.max')
186 },
187 PLUGINS: {
188 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.plugins.window')),
189 MAX: config.get<number>('rates_limit.plugins.max')
190 },
191 WELL_KNOWN: {
192 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.well_known.window')),
193 MAX: config.get<number>('rates_limit.well_known.max')
194 },
195 FEEDS: {
196 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.feeds.window')),
197 MAX: config.get<number>('rates_limit.feeds.max')
198 },
199 ACTIVITY_PUB: {
200 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.activity_pub.window')),
201 MAX: config.get<number>('rates_limit.activity_pub.max')
202 },
203 CLIENT: {
204 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.client.window')),
205 MAX: config.get<number>('rates_limit.client.max')
206 }
207 },
208 TRUST_PROXY: config.get<string[]>('trust_proxy'),
209 LOG: {
210 LEVEL: config.get<string>('log.level'),
211 ROTATION: {
212 ENABLED: config.get<boolean>('log.rotation.enabled'),
213 MAX_FILE_SIZE: bytes.parse(config.get<string>('log.rotation.max_file_size')),
214 MAX_FILES: config.get<number>('log.rotation.max_files')
215 },
216 ANONYMIZE_IP: config.get<boolean>('log.anonymize_ip'),
217 LOG_PING_REQUESTS: config.get<boolean>('log.log_ping_requests'),
218 LOG_TRACKER_UNKNOWN_INFOHASH: config.get<boolean>('log.log_tracker_unknown_infohash'),
219 PRETTIFY_SQL: config.get<boolean>('log.prettify_sql'),
220 ACCEPT_CLIENT_LOG: config.get<boolean>('log.accept_client_log')
221 },
222 OPEN_TELEMETRY: {
223 METRICS: {
224 ENABLED: config.get<boolean>('open_telemetry.metrics.enabled'),
225
226 HTTP_REQUEST_DURATION: {
227 ENABLED: config.get<boolean>('open_telemetry.metrics.http_request_duration.enabled')
228 },
229
230 PROMETHEUS_EXPORTER: {
231 HOSTNAME: config.get<string>('open_telemetry.metrics.prometheus_exporter.hostname'),
232 PORT: config.get<number>('open_telemetry.metrics.prometheus_exporter.port')
233 }
234 },
235 TRACING: {
236 ENABLED: config.get<boolean>('open_telemetry.tracing.enabled'),
237
238 JAEGER_EXPORTER: {
239 ENDPOINT: config.get<string>('open_telemetry.tracing.jaeger_exporter.endpoint')
240 }
241 }
242 },
243 TRENDING: {
244 VIDEOS: {
245 INTERVAL_DAYS: config.get<number>('trending.videos.interval_days'),
246 ALGORITHMS: {
247 get ENABLED () { return config.get<string[]>('trending.videos.algorithms.enabled') },
248 get DEFAULT () { return config.get<string>('trending.videos.algorithms.default') }
249 }
250 }
251 },
252 REDUNDANCY: {
253 VIDEOS: {
254 CHECK_INTERVAL: parseDurationToMs(config.get<string>('redundancy.videos.check_interval')),
255 STRATEGIES: buildVideosRedundancy(config.get<any[]>('redundancy.videos.strategies'))
256 }
257 },
258 REMOTE_REDUNDANCY: {
259 VIDEOS: {
260 ACCEPT_FROM: config.get<VideoRedundancyConfigFilter>('remote_redundancy.videos.accept_from')
261 }
262 },
263 CSP: {
264 ENABLED: config.get<boolean>('csp.enabled'),
265 REPORT_ONLY: config.get<boolean>('csp.report_only'),
266 REPORT_URI: config.get<string>('csp.report_uri')
267 },
268 SECURITY: {
269 FRAMEGUARD: {
270 ENABLED: config.get<boolean>('security.frameguard.enabled')
271 },
272 POWERED_BY_HEADER: {
273 ENABLED: config.get<boolean>('security.powered_by_header.enabled')
274 }
275 },
276 TRACKER: {
277 ENABLED: config.get<boolean>('tracker.enabled'),
278 PRIVATE: config.get<boolean>('tracker.private'),
279 REJECT_TOO_MANY_ANNOUNCES: config.get<boolean>('tracker.reject_too_many_announces')
280 },
281 HISTORY: {
282 VIDEOS: {
283 MAX_AGE: parseDurationToMs(config.get('history.videos.max_age'))
284 }
285 },
286 VIEWS: {
287 VIDEOS: {
288 REMOTE: {
289 MAX_AGE: parseDurationToMs(config.get('views.videos.remote.max_age'))
290 },
291 LOCAL_BUFFER_UPDATE_INTERVAL: parseDurationToMs(config.get('views.videos.local_buffer_update_interval')),
292 IP_VIEW_EXPIRATION: parseDurationToMs(config.get('views.videos.ip_view_expiration'))
293 }
294 },
295 GEO_IP: {
296 ENABLED: config.get<boolean>('geo_ip.enabled'),
297 COUNTRY: {
298 DATABASE_URL: config.get<string>('geo_ip.country.database_url')
299 }
300 },
301 PLUGINS: {
302 INDEX: {
303 ENABLED: config.get<boolean>('plugins.index.enabled'),
304 CHECK_LATEST_VERSIONS_INTERVAL: parseDurationToMs(config.get<string>('plugins.index.check_latest_versions_interval')),
305 URL: config.get<string>('plugins.index.url')
306 }
307 },
308 FEDERATION: {
309 VIDEOS: {
310 FEDERATE_UNLISTED: config.get<boolean>('federation.videos.federate_unlisted'),
311 CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions')
312 },
313 SIGN_FEDERATED_FETCHES: config.get<boolean>('federation.sign_federated_fetches')
314 },
315 PEERTUBE: {
316 CHECK_LATEST_VERSION: {
317 ENABLED: config.get<boolean>('peertube.check_latest_version.enabled'),
318 URL: config.get<string>('peertube.check_latest_version.url')
319 }
320 },
321 WEBADMIN: {
322 CONFIGURATION: {
323 EDITION: {
324 ALLOWED: config.get<boolean>('webadmin.configuration.edition.allowed')
325 }
326 }
327 },
328 FEEDS: {
329 VIDEOS: {
330 COUNT: config.get<number>('feeds.videos.count')
331 },
332 COMMENTS: {
333 COUNT: config.get<number>('feeds.comments.count')
334 }
335 },
336 REMOTE_RUNNERS: {
337 STALLED_JOBS: {
338 LIVE: parseDurationToMs(config.get<string>('remote_runners.stalled_jobs.live')),
339 VOD: parseDurationToMs(config.get<string>('remote_runners.stalled_jobs.vod'))
340 }
341 },
342 ADMIN: {
343 get EMAIL () { return config.get<string>('admin.email') }
344 },
345 CONTACT_FORM: {
346 get ENABLED () { return config.get<boolean>('contact_form.enabled') }
347 },
348 SIGNUP: {
349 get ENABLED () { return config.get<boolean>('signup.enabled') },
350 get REQUIRES_APPROVAL () { return config.get<boolean>('signup.requires_approval') },
351 get LIMIT () { return config.get<number>('signup.limit') },
352 get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') },
353 get MINIMUM_AGE () { return config.get<number>('signup.minimum_age') },
354 FILTERS: {
355 CIDR: {
356 get WHITELIST () { return config.get<string[]>('signup.filters.cidr.whitelist') },
357 get BLACKLIST () { return config.get<string[]>('signup.filters.cidr.blacklist') }
358 }
359 }
360 },
361 USER: {
362 HISTORY: {
363 VIDEOS: {
364 get ENABLED () { return config.get<boolean>('user.history.videos.enabled') }
365 }
366 },
367 get VIDEO_QUOTA () { return parseBytes(config.get<number>('user.video_quota')) },
368 get VIDEO_QUOTA_DAILY () { return parseBytes(config.get<number>('user.video_quota_daily')) }
369 },
370 VIDEO_CHANNELS: {
371 get MAX_PER_USER () { return config.get<number>('video_channels.max_per_user') }
372 },
373 TRANSCODING: {
374 get ENABLED () { return config.get<boolean>('transcoding.enabled') },
375 get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
376 get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') },
377 get THREADS () { return config.get<number>('transcoding.threads') },
378 get CONCURRENCY () { return config.get<number>('transcoding.concurrency') },
379 get PROFILE () { return config.get<string>('transcoding.profile') },
380 get ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION () { return config.get<boolean>('transcoding.always_transcode_original_resolution') },
381 RESOLUTIONS: {
382 get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') },
383 get '144p' () { return config.get<boolean>('transcoding.resolutions.144p') },
384 get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
385 get '360p' () { return config.get<boolean>('transcoding.resolutions.360p') },
386 get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') },
387 get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') },
388 get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') },
389 get '1440p' () { return config.get<boolean>('transcoding.resolutions.1440p') },
390 get '2160p' () { return config.get<boolean>('transcoding.resolutions.2160p') }
391 },
392 HLS: {
393 get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') }
394 },
395 WEB_VIDEOS: {
396 get ENABLED () { return config.get<boolean>('transcoding.web_videos.enabled') }
397 },
398 REMOTE_RUNNERS: {
399 get ENABLED () { return config.get<boolean>('transcoding.remote_runners.enabled') }
400 }
401 },
402 LIVE: {
403 get ENABLED () { return config.get<boolean>('live.enabled') },
404
405 get MAX_DURATION () { return parseDurationToMs(config.get<string>('live.max_duration')) },
406 get MAX_INSTANCE_LIVES () { return config.get<number>('live.max_instance_lives') },
407 get MAX_USER_LIVES () { return config.get<number>('live.max_user_lives') },
408
409 get ALLOW_REPLAY () { return config.get<boolean>('live.allow_replay') },
410
411 LATENCY_SETTING: {
412 get ENABLED () { return config.get<boolean>('live.latency_setting.enabled') }
413 },
414
415 RTMP: {
416 get ENABLED () { return config.get<boolean>('live.rtmp.enabled') },
417 get PORT () { return config.get<number>('live.rtmp.port') },
418 get HOSTNAME () { return config.get<number>('live.rtmp.hostname') },
419 get PUBLIC_HOSTNAME () { return config.get<number>('live.rtmp.public_hostname') }
420 },
421
422 RTMPS: {
423 get ENABLED () { return config.get<boolean>('live.rtmps.enabled') },
424 get PORT () { return config.get<number>('live.rtmps.port') },
425 get HOSTNAME () { return config.get<number>('live.rtmps.hostname') },
426 get PUBLIC_HOSTNAME () { return config.get<number>('live.rtmps.public_hostname') },
427 get KEY_FILE () { return config.get<string>('live.rtmps.key_file') },
428 get CERT_FILE () { return config.get<string>('live.rtmps.cert_file') }
429 },
430
431 TRANSCODING: {
432 get ENABLED () { return config.get<boolean>('live.transcoding.enabled') },
433 get THREADS () { return config.get<number>('live.transcoding.threads') },
434 get PROFILE () { return config.get<string>('live.transcoding.profile') },
435
436 get ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION () { return config.get<boolean>('live.transcoding.always_transcode_original_resolution') },
437
438 RESOLUTIONS: {
439 get '144p' () { return config.get<boolean>('live.transcoding.resolutions.144p') },
440 get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') },
441 get '360p' () { return config.get<boolean>('live.transcoding.resolutions.360p') },
442 get '480p' () { return config.get<boolean>('live.transcoding.resolutions.480p') },
443 get '720p' () { return config.get<boolean>('live.transcoding.resolutions.720p') },
444 get '1080p' () { return config.get<boolean>('live.transcoding.resolutions.1080p') },
445 get '1440p' () { return config.get<boolean>('live.transcoding.resolutions.1440p') },
446 get '2160p' () { return config.get<boolean>('live.transcoding.resolutions.2160p') }
447 },
448 REMOTE_RUNNERS: {
449 get ENABLED () { return config.get<boolean>('live.transcoding.remote_runners.enabled') }
450 }
451 }
452 },
453 VIDEO_STUDIO: {
454 get ENABLED () { return config.get<boolean>('video_studio.enabled') },
455 REMOTE_RUNNERS: {
456 get ENABLED () { return config.get<boolean>('video_studio.remote_runners.enabled') }
457 }
458 },
459 VIDEO_FILE: {
460 UPDATE: {
461 get ENABLED () { return config.get<boolean>('video_file.update.enabled') }
462 }
463 },
464 IMPORT: {
465 VIDEOS: {
466 get CONCURRENCY () { return config.get<number>('import.videos.concurrency') },
467 get TIMEOUT () { return parseDurationToMs(config.get<string>('import.videos.timeout')) },
468
469 HTTP: {
470 get ENABLED () { return config.get<boolean>('import.videos.http.enabled') },
471
472 YOUTUBE_DL_RELEASE: {
473 get URL () { return config.get<string>('import.videos.http.youtube_dl_release.url') },
474 get NAME () { return config.get<string>('import.videos.http.youtube_dl_release.name') },
475 get PYTHON_PATH () { return config.get<string>('import.videos.http.youtube_dl_release.python_path') }
476 },
477
478 get FORCE_IPV4 () { return config.get<boolean>('import.videos.http.force_ipv4') }
479 },
480 TORRENT: {
481 get ENABLED () { return config.get<boolean>('import.videos.torrent.enabled') }
482 }
483 },
484 VIDEO_CHANNEL_SYNCHRONIZATION: {
485 get ENABLED () { return config.get<boolean>('import.video_channel_synchronization.enabled') },
486 get MAX_PER_USER () { return config.get<number>('import.video_channel_synchronization.max_per_user') },
487 get CHECK_INTERVAL () { return parseDurationToMs(config.get<string>('import.video_channel_synchronization.check_interval')) },
488 get VIDEOS_LIMIT_PER_SYNCHRONIZATION () {
489 return config.get<number>('import.video_channel_synchronization.videos_limit_per_synchronization')
490 },
491 get FULL_SYNC_VIDEOS_LIMIT () {
492 return config.get<number>('import.video_channel_synchronization.full_sync_videos_limit')
493 }
494 }
495 },
496 AUTO_BLACKLIST: {
497 VIDEOS: {
498 OF_USERS: {
499 get ENABLED () { return config.get<boolean>('auto_blacklist.videos.of_users.enabled') }
500 }
501 }
502 },
503 CACHE: {
504 PREVIEWS: {
505 get SIZE () { return config.get<number>('cache.previews.size') }
506 },
507 VIDEO_CAPTIONS: {
508 get SIZE () { return config.get<number>('cache.captions.size') }
509 },
510 TORRENTS: {
511 get SIZE () { return config.get<number>('cache.torrents.size') }
512 },
513 STORYBOARDS: {
514 get SIZE () { return config.get<number>('cache.storyboards.size') }
515 }
516 },
517 INSTANCE: {
518 get NAME () { return config.get<string>('instance.name') },
519 get SHORT_DESCRIPTION () { return config.get<string>('instance.short_description') },
520 get DESCRIPTION () { return config.get<string>('instance.description') },
521 get TERMS () { return config.get<string>('instance.terms') },
522 get CODE_OF_CONDUCT () { return config.get<string>('instance.code_of_conduct') },
523
524 get CREATION_REASON () { return config.get<string>('instance.creation_reason') },
525
526 get MODERATION_INFORMATION () { return config.get<string>('instance.moderation_information') },
527 get ADMINISTRATOR () { return config.get<string>('instance.administrator') },
528 get MAINTENANCE_LIFETIME () { return config.get<string>('instance.maintenance_lifetime') },
529 get BUSINESS_MODEL () { return config.get<string>('instance.business_model') },
530 get HARDWARE_INFORMATION () { return config.get<string>('instance.hardware_information') },
531
532 get LANGUAGES () { return config.get<string[]>('instance.languages') || [] },
533 get CATEGORIES () { return config.get<number[]>('instance.categories') || [] },
534
535 get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') },
536 get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
537
538 get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
539
540 CUSTOMIZATIONS: {
541 get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
542 get CSS () { return config.get<string>('instance.customizations.css') }
543 },
544 get ROBOTS () { return config.get<string>('instance.robots') },
545 get SECURITYTXT () { return config.get<string>('instance.securitytxt') },
546 get SECURITYTXT_CONTACT () { return config.get<string>('admin.email') }
547 },
548 SERVICES: {
549 TWITTER: {
550 get USERNAME () { return config.get<string>('services.twitter.username') },
551 get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') }
552 }
553 },
554 FOLLOWERS: {
555 INSTANCE: {
556 get ENABLED () { return config.get<boolean>('followers.instance.enabled') },
557 get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') }
558 }
559 },
560 FOLLOWINGS: {
561 INSTANCE: {
562 AUTO_FOLLOW_BACK: {
563 get ENABLED () {
564 return config.get<boolean>('followings.instance.auto_follow_back.enabled')
565 }
566 },
567 AUTO_FOLLOW_INDEX: {
568 get ENABLED () {
569 return config.get<boolean>('followings.instance.auto_follow_index.enabled')
570 },
571 get INDEX_URL () {
572 return config.get<string>('followings.instance.auto_follow_index.index_url')
573 }
574 }
575 }
576 },
577 THEME: {
578 get DEFAULT () { return config.get<string>('theme.default') }
579 },
580 BROADCAST_MESSAGE: {
581 get ENABLED () { return config.get<boolean>('broadcast_message.enabled') },
582 get MESSAGE () { return config.get<string>('broadcast_message.message') },
583 get LEVEL () { return config.get<BroadcastMessageLevel>('broadcast_message.level') },
584 get DISMISSABLE () { return config.get<boolean>('broadcast_message.dismissable') }
585 },
586 SEARCH: {
587 REMOTE_URI: {
588 get USERS () { return config.get<boolean>('search.remote_uri.users') },
589 get ANONYMOUS () { return config.get<boolean>('search.remote_uri.anonymous') }
590 },
591 SEARCH_INDEX: {
592 get ENABLED () { return config.get<boolean>('search.search_index.enabled') },
593 get URL () { return config.get<string>('search.search_index.url') },
594 get DISABLE_LOCAL_SEARCH () { return config.get<boolean>('search.search_index.disable_local_search') },
595 get IS_DEFAULT_SEARCH () { return config.get<boolean>('search.search_index.is_default_search') }
596 }
597 }
598
599}
600
601function registerConfigChangedHandler (fun: Function) {
602 configChangedHandlers.push(fun)
603}
604
605function isEmailEnabled () {
606 if (CONFIG.SMTP.TRANSPORT === 'sendmail' && CONFIG.SMTP.SENDMAIL) return true
607
608 if (CONFIG.SMTP.TRANSPORT === 'smtp' && CONFIG.SMTP.HOSTNAME && CONFIG.SMTP.PORT) return true
609
610 return false
611}
612
613function getLocalConfigFilePath () {
614 const localConfigDir = getLocalConfigDir()
615
616 let filename = 'local'
617 if (process.env.NODE_ENV) filename += `-${process.env.NODE_ENV}`
618 if (process.env.NODE_APP_INSTANCE) filename += `-${process.env.NODE_APP_INSTANCE}`
619
620 return join(localConfigDir, filename + '.json')
621}
622
623// ---------------------------------------------------------------------------
624
625export {
626 CONFIG,
627 getLocalConfigFilePath,
628 registerConfigChangedHandler,
629 isEmailEnabled
630}
631
632// ---------------------------------------------------------------------------
633
634function getLocalConfigDir () {
635 if (process.env.PEERTUBE_LOCAL_CONFIG) return process.env.PEERTUBE_LOCAL_CONFIG
636
637 const configSources = config.util.getConfigSources()
638 if (configSources.length === 0) throw new Error('Invalid config source.')
639
640 return dirname(configSources[0].name)
641}
642
643function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] {
644 if (!objs) return []
645
646 if (!Array.isArray(objs)) return objs
647
648 return objs.map(obj => {
649 return Object.assign({}, obj, {
650 minLifetime: parseDurationToMs(obj.min_lifetime),
651 size: bytes.parse(obj.size),
652 minViews: obj.min_views
653 })
654 })
655}
656
657export function reloadConfig () {
658
659 function getConfigDirectories () {
660 if (process.env.NODE_CONFIG_DIR) {
661 return process.env.NODE_CONFIG_DIR.split(':')
662 }
663
664 return [ join(root(), 'config') ]
665 }
666
667 function purge () {
668 const directories = getConfigDirectories()
669
670 for (const fileName in require.cache) {
671 if (directories.some((dir) => fileName.includes(dir)) === false) {
672 continue
673 }
674
675 delete require.cache[fileName]
676 }
677
678 decacheModule('config')
679 }
680
681 purge()
682
683 config = require('config')
684
685 for (const configChangedHandler of configChangedHandlers) {
686 configChangedHandler()
687 }
688}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
deleted file mode 100644
index de5f11f8f..000000000
--- a/server/initializers/constants.ts
+++ /dev/null
@@ -1,1396 +0,0 @@
1import { RepeatOptions } from 'bullmq'
2import { Encoding, randomBytes } from 'crypto'
3import { invert } from 'lodash'
4import { join } from 'path'
5import { randomInt, root } from '@shared/core-utils'
6import {
7 AbuseState,
8 JobType,
9 RunnerJobState,
10 UserRegistrationState,
11 VideoChannelSyncState,
12 VideoImportState,
13 VideoPrivacy,
14 VideoRateType,
15 VideoResolution,
16 VideoState,
17 VideoTranscodingFPS
18} from '../../shared/models'
19import { ActivityPubActorType } from '../../shared/models/activitypub'
20import { ActorImageType, FollowState } from '../../shared/models/actors'
21import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
22import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
23import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
24// Do not use barrels, remain constants as independent as possible
25import { isTestInstance, isTestOrDevInstance, parseDurationToMs, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
26import { CONFIG, registerConfigChangedHandler } from './config'
27
28// ---------------------------------------------------------------------------
29
30const LAST_MIGRATION_VERSION = 800
31
32// ---------------------------------------------------------------------------
33
34const API_VERSION = 'v1'
35const PEERTUBE_VERSION: string = require(join(root(), 'package.json')).version
36
37const PAGINATION = {
38 GLOBAL: {
39 COUNT: {
40 DEFAULT: 15,
41 MAX: 100
42 }
43 },
44 OUTBOX: {
45 COUNT: {
46 MAX: 50
47 }
48 }
49}
50
51const WEBSERVER = {
52 URL: '',
53 HOST: '',
54 SCHEME: '',
55 WS: '',
56 HOSTNAME: '',
57 PORT: 0,
58
59 RTMP_URL: '',
60 RTMPS_URL: '',
61
62 RTMP_BASE_LIVE_URL: '',
63 RTMPS_BASE_LIVE_URL: ''
64}
65
66// Sortable columns per schema
67const SORTABLE_COLUMNS = {
68 ADMIN_USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ],
69 USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
70 ACCOUNTS: [ 'createdAt' ],
71 JOBS: [ 'createdAt' ],
72 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
73 VIDEO_IMPORTS: [ 'createdAt' ],
74 VIDEO_CHANNEL_SYNCS: [ 'externalChannelUrl', 'videoChannel', 'createdAt', 'lastSyncAt', 'state' ],
75
76 VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ],
77 VIDEO_COMMENTS: [ 'createdAt' ],
78
79 VIDEO_PASSWORDS: [ 'createdAt' ],
80
81 VIDEO_RATES: [ 'createdAt' ],
82 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
83
84 INSTANCE_FOLLOWERS: [ 'createdAt', 'state', 'score' ],
85 INSTANCE_FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ],
86 ACCOUNT_FOLLOWERS: [ 'createdAt' ],
87 CHANNEL_FOLLOWERS: [ 'createdAt' ],
88
89 USER_REGISTRATIONS: [ 'createdAt', 'state' ],
90
91 RUNNERS: [ 'createdAt' ],
92 RUNNER_REGISTRATION_TOKENS: [ 'createdAt' ],
93 RUNNER_JOBS: [ 'updatedAt', 'createdAt', 'priority', 'state', 'progress' ],
94
95 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ],
96
97 // Don't forget to update peertube-search-index with the same values
98 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
99 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
100 VIDEO_PLAYLISTS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
101
102 ABUSES: [ 'id', 'createdAt', 'state' ],
103
104 ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
105 SERVERS_BLOCKLIST: [ 'createdAt' ],
106
107 USER_NOTIFICATIONS: [ 'createdAt', 'read' ],
108
109 VIDEO_PLAYLISTS: [ 'name', 'displayName', 'createdAt', 'updatedAt' ],
110
111 PLUGINS: [ 'name', 'createdAt', 'updatedAt' ],
112
113 AVAILABLE_PLUGINS: [ 'npmName', 'popularity' ],
114
115 VIDEO_REDUNDANCIES: [ 'name' ]
116}
117
118const ROUTE_CACHE_LIFETIME = {
119 FEEDS: '15 minutes',
120 ROBOTS: '2 hours',
121 SITEMAP: '1 day',
122 SECURITYTXT: '2 hours',
123 NODEINFO: '10 minutes',
124 DNT_POLICY: '1 week',
125 ACTIVITY_PUB: {
126 VIDEOS: '1 second' // 1 second, cache concurrent requests after a broadcast for example
127 },
128 STATS: '4 hours',
129 WELL_KNOWN: '1 day'
130}
131
132// ---------------------------------------------------------------------------
133
134// Number of points we add/remove after a successful/bad request
135const ACTOR_FOLLOW_SCORE = {
136 PENALTY: -10,
137 BONUS: 10,
138 BASE: 1000,
139 MAX: 10000
140}
141
142const FOLLOW_STATES: { [ id: string ]: FollowState } = {
143 PENDING: 'pending',
144 ACCEPTED: 'accepted',
145 REJECTED: 'rejected'
146}
147
148const REMOTE_SCHEME = {
149 HTTP: 'https',
150 WS: 'wss'
151}
152
153// ---------------------------------------------------------------------------
154
155const JOB_ATTEMPTS: { [id in JobType]: number } = {
156 'activitypub-http-broadcast': 1,
157 'activitypub-http-broadcast-parallel': 1,
158 'activitypub-http-unicast': 1,
159 'activitypub-http-fetcher': 2,
160 'activitypub-follow': 5,
161 'activitypub-cleaner': 1,
162 'video-file-import': 1,
163 'video-transcoding': 1,
164 'video-import': 1,
165 'email': 5,
166 'actor-keys': 3,
167 'videos-views-stats': 1,
168 'activitypub-refresher': 1,
169 'video-redundancy': 1,
170 'video-live-ending': 1,
171 'video-studio-edition': 1,
172 'manage-video-torrent': 1,
173 'video-channel-import': 1,
174 'after-video-channel-import': 1,
175 'move-to-object-storage': 3,
176 'transcoding-job-builder': 1,
177 'generate-video-storyboard': 1,
178 'notify': 1,
179 'federate-video': 1
180}
181// Excluded keys are jobs that can be configured by admins
182const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-import'>]: number } = {
183 'activitypub-http-broadcast': 1,
184 'activitypub-http-broadcast-parallel': 30,
185 'activitypub-http-unicast': 30,
186 'activitypub-http-fetcher': 3,
187 'activitypub-cleaner': 1,
188 'activitypub-follow': 1,
189 'video-file-import': 1,
190 'email': 5,
191 'actor-keys': 1,
192 'videos-views-stats': 1,
193 'activitypub-refresher': 1,
194 'video-redundancy': 1,
195 'video-live-ending': 10,
196 'video-studio-edition': 1,
197 'manage-video-torrent': 1,
198 'move-to-object-storage': 1,
199 'video-channel-import': 1,
200 'after-video-channel-import': 1,
201 'transcoding-job-builder': 1,
202 'generate-video-storyboard': 1,
203 'notify': 5,
204 'federate-video': 3
205}
206const JOB_TTL: { [id in JobType]: number } = {
207 'activitypub-http-broadcast': 60000 * 10, // 10 minutes
208 'activitypub-http-broadcast-parallel': 60000 * 10, // 10 minutes
209 'activitypub-http-unicast': 60000 * 10, // 10 minutes
210 'activitypub-http-fetcher': 1000 * 3600 * 10, // 10 hours
211 'activitypub-follow': 60000 * 10, // 10 minutes
212 'activitypub-cleaner': 1000 * 3600, // 1 hour
213 'video-file-import': 1000 * 3600, // 1 hour
214 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long
215 'video-studio-edition': 1000 * 3600 * 10, // 10 hours
216 'video-import': CONFIG.IMPORT.VIDEOS.TIMEOUT,
217 'email': 60000 * 10, // 10 minutes
218 'actor-keys': 60000 * 20, // 20 minutes
219 'videos-views-stats': undefined, // Unlimited
220 'activitypub-refresher': 60000 * 10, // 10 minutes
221 'video-redundancy': 1000 * 3600 * 3, // 3 hours
222 'video-live-ending': 1000 * 60 * 10, // 10 minutes
223 'generate-video-storyboard': 1000 * 60 * 10, // 10 minutes
224 'manage-video-torrent': 1000 * 3600 * 3, // 3 hours
225 'move-to-object-storage': 1000 * 60 * 60 * 3, // 3 hours
226 'video-channel-import': 1000 * 60 * 60 * 4, // 4 hours
227 'after-video-channel-import': 60000 * 5, // 5 minutes
228 'transcoding-job-builder': 60000, // 1 minute
229 'notify': 60000 * 5, // 5 minutes
230 'federate-video': 60000 * 5 // 5 minutes
231}
232const REPEAT_JOBS: { [ id in JobType ]?: RepeatOptions } = {
233 'videos-views-stats': {
234 pattern: randomInt(1, 20) + ' * * * *' // Between 1-20 minutes past the hour
235 },
236 'activitypub-cleaner': {
237 pattern: '30 5 * * ' + randomInt(0, 7) // 1 time per week (random day) at 5:30 AM
238 }
239}
240const JOB_PRIORITY = {
241 TRANSCODING: 100,
242 VIDEO_STUDIO: 150
243}
244
245const JOB_REMOVAL_OPTIONS = {
246 COUNT: 10000, // Max jobs to store
247
248 SUCCESS: { // Success jobs
249 'DEFAULT': parseDurationToMs('2 days'),
250
251 'activitypub-http-broadcast-parallel': parseDurationToMs('10 minutes'),
252 'activitypub-http-unicast': parseDurationToMs('1 hour'),
253 'videos-views-stats': parseDurationToMs('3 hours'),
254 'activitypub-refresher': parseDurationToMs('10 hours')
255 },
256
257 FAILURE: { // Failed job
258 DEFAULT: parseDurationToMs('7 days')
259 }
260}
261
262const VIDEO_IMPORT_TIMEOUT = Math.floor(JOB_TTL['video-import'] * 0.9)
263
264const RUNNER_JOBS = {
265 MAX_FAILURES: 5,
266 LAST_CONTACT_UPDATE_INTERVAL: 30000
267}
268
269// ---------------------------------------------------------------------------
270
271const BROADCAST_CONCURRENCY = 30 // How many requests in parallel we do in activitypub-http-broadcast job
272const CRAWL_REQUEST_CONCURRENCY = 1 // How many requests in parallel to fetch remote data (likes, shares...)
273
274const AP_CLEANER = {
275 CONCURRENCY: 10, // How many requests in parallel we do in activitypub-cleaner job
276 UNAVAILABLE_TRESHOLD: 3, // How many attempts we do before removing an unavailable remote resource
277 PERIOD: parseDurationToMs('1 week') // /!\ Has to be sync with REPEAT_JOBS
278}
279
280const REQUEST_TIMEOUTS = {
281 DEFAULT: 7000, // 7 seconds
282 FILE: 30000, // 30 seconds
283 REDUNDANCY: JOB_TTL['video-redundancy']
284}
285
286const SCHEDULER_INTERVALS_MS = {
287 RUNNER_JOB_WATCH_DOG: Math.min(CONFIG.REMOTE_RUNNERS.STALLED_JOBS.VOD, CONFIG.REMOTE_RUNNERS.STALLED_JOBS.LIVE),
288 ACTOR_FOLLOW_SCORES: 60000 * 60, // 1 hour
289 REMOVE_OLD_JOBS: 60000 * 60, // 1 hour
290 UPDATE_VIDEOS: 60000, // 1 minute
291 YOUTUBE_DL_UPDATE: 60000 * 60 * 24, // 1 day
292 GEO_IP_UPDATE: 60000 * 60 * 24, // 1 day
293 VIDEO_VIEWS_BUFFER_UPDATE: CONFIG.VIEWS.VIDEOS.LOCAL_BUFFER_UPDATE_INTERVAL,
294 CHECK_PLUGINS: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL,
295 CHECK_PEERTUBE_VERSION: 60000 * 60 * 24, // 1 day
296 AUTO_FOLLOW_INDEX_INSTANCES: 60000 * 60 * 24, // 1 day
297 REMOVE_OLD_VIEWS: 60000 * 60 * 24, // 1 day
298 REMOVE_OLD_HISTORY: 60000 * 60 * 24, // 1 day
299 UPDATE_INBOX_STATS: 1000 * 60, // 1 minute
300 REMOVE_DANGLING_RESUMABLE_UPLOADS: 60000 * 60, // 1 hour
301 CHANNEL_SYNC_CHECK_INTERVAL: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.CHECK_INTERVAL
302}
303
304// ---------------------------------------------------------------------------
305
306const CONSTRAINTS_FIELDS = {
307 USERS: {
308 NAME: { min: 1, max: 120 }, // Length
309 DESCRIPTION: { min: 3, max: 1000 }, // Length
310 USERNAME: { min: 1, max: 50 }, // Length
311 PASSWORD: { min: 6, max: 255 }, // Length
312 VIDEO_QUOTA: { min: -1 },
313 VIDEO_QUOTA_DAILY: { min: -1 },
314 VIDEO_LANGUAGES: { max: 500 }, // Array length
315 BLOCKED_REASON: { min: 3, max: 250 } // Length
316 },
317 ABUSES: {
318 REASON: { min: 2, max: 3000 }, // Length
319 MODERATION_COMMENT: { min: 2, max: 3000 } // Length
320 },
321 ABUSE_MESSAGES: {
322 MESSAGE: { min: 2, max: 3000 } // Length
323 },
324 USER_REGISTRATIONS: {
325 REASON_MESSAGE: { min: 2, max: 3000 }, // Length
326 MODERATOR_MESSAGE: { min: 2, max: 3000 } // Length
327 },
328 VIDEO_BLACKLIST: {
329 REASON: { min: 2, max: 300 } // Length
330 },
331 VIDEO_CHANNELS: {
332 NAME: { min: 1, max: 120 }, // Length
333 DESCRIPTION: { min: 3, max: 1000 }, // Length
334 SUPPORT: { min: 3, max: 1000 }, // Length
335 EXTERNAL_CHANNEL_URL: { min: 3, max: 2000 }, // Length
336 URL: { min: 3, max: 2000 } // Length
337 },
338 VIDEO_CHANNEL_SYNCS: {
339 EXTERNAL_CHANNEL_URL: { min: 3, max: 2000 } // Length
340 },
341 VIDEO_CAPTIONS: {
342 CAPTION_FILE: {
343 EXTNAME: [ '.vtt', '.srt' ],
344 FILE_SIZE: {
345 max: 20 * 1024 * 1024 // 20MB
346 }
347 }
348 },
349 VIDEO_IMPORTS: {
350 URL: { min: 3, max: 2000 }, // Length
351 TORRENT_NAME: { min: 3, max: 255 }, // Length
352 TORRENT_FILE: {
353 EXTNAME: [ '.torrent' ],
354 FILE_SIZE: {
355 max: 1024 * 200 // 200 KB
356 }
357 }
358 },
359 VIDEOS_REDUNDANCY: {
360 URL: { min: 3, max: 2000 } // Length
361 },
362 VIDEO_RATES: {
363 URL: { min: 3, max: 2000 } // Length
364 },
365 VIDEOS: {
366 NAME: { min: 3, max: 120 }, // Length
367 LANGUAGE: { min: 1, max: 10 }, // Length
368 TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length
369 DESCRIPTION: { min: 3, max: 10000 }, // Length
370 SUPPORT: { min: 3, max: 1000 }, // Length
371 IMAGE: {
372 EXTNAME: [ '.png', '.jpg', '.jpeg', '.webp' ],
373 FILE_SIZE: {
374 max: 4 * 1024 * 1024 // 4MB
375 }
376 },
377 EXTNAME: [] as string[],
378 INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2
379 DURATION: { min: 0 }, // Number
380 TAGS: { min: 0, max: 5 }, // Number of total tags
381 TAG: { min: 2, max: 30 }, // Length
382 VIEWS: { min: 0 },
383 LIKES: { min: 0 },
384 DISLIKES: { min: 0 },
385 FILE_SIZE: { min: -1 },
386 PARTIAL_UPLOAD_SIZE: { max: 50 * 1024 * 1024 * 1024 }, // 50GB
387 URL: { min: 3, max: 2000 } // Length
388 },
389 VIDEO_PLAYLISTS: {
390 NAME: { min: 1, max: 120 }, // Length
391 DESCRIPTION: { min: 3, max: 1000 }, // Length
392 URL: { min: 3, max: 2000 }, // Length
393 IMAGE: {
394 EXTNAME: [ '.jpg', '.jpeg' ],
395 FILE_SIZE: {
396 max: 4 * 1024 * 1024 // 4MB
397 }
398 }
399 },
400 ACTORS: {
401 PUBLIC_KEY: { min: 10, max: 5000 }, // Length
402 PRIVATE_KEY: { min: 10, max: 5000 }, // Length
403 URL: { min: 3, max: 2000 }, // Length
404 IMAGE: {
405 EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ],
406 FILE_SIZE: {
407 max: 4 * 1024 * 1024 // 4MB
408 }
409 }
410 },
411 VIDEO_EVENTS: {
412 COUNT: { min: 0 }
413 },
414 VIDEO_COMMENTS: {
415 TEXT: { min: 1, max: 10000 }, // Length
416 URL: { min: 3, max: 2000 } // Length
417 },
418 VIDEO_SHARE: {
419 URL: { min: 3, max: 2000 } // Length
420 },
421 CONTACT_FORM: {
422 FROM_NAME: { min: 1, max: 120 }, // Length
423 BODY: { min: 3, max: 5000 } // Length
424 },
425 PLUGINS: {
426 NAME: { min: 1, max: 214 }, // Length
427 DESCRIPTION: { min: 1, max: 20000 } // Length
428 },
429 COMMONS: {
430 URL: { min: 5, max: 2000 } // Length
431 },
432 VIDEO_STUDIO: {
433 TASKS: { min: 1, max: 10 }, // Number of tasks
434 CUT_TIME: { min: 0 } // Value
435 },
436 LOGS: {
437 CLIENT_MESSAGE: { min: 1, max: 1000 }, // Length
438 CLIENT_STACK_TRACE: { min: 1, max: 15000 }, // Length
439 CLIENT_META: { min: 1, max: 5000 }, // Length
440 CLIENT_USER_AGENT: { min: 1, max: 200 } // Length
441 },
442 RUNNERS: {
443 TOKEN: { min: 1, max: 1000 }, // Length
444 NAME: { min: 1, max: 100 }, // Length
445 DESCRIPTION: { min: 1, max: 1000 } // Length
446 },
447 RUNNER_JOBS: {
448 TOKEN: { min: 1, max: 1000 }, // Length
449 REASON: { min: 1, max: 5000 }, // Length
450 ERROR_MESSAGE: { min: 1, max: 5000 }, // Length
451 PROGRESS: { min: 0, max: 100 } // Value
452 },
453 VIDEO_PASSWORD: {
454 LENGTH: { min: 2, max: 100 }
455 }
456}
457
458const VIEW_LIFETIME = {
459 VIEW: CONFIG.VIEWS.VIDEOS.IP_VIEW_EXPIRATION,
460 VIEWER_COUNTER: 60000 * 2, // 2 minutes
461 VIEWER_STATS: 60000 * 60 // 1 hour
462}
463
464const MAX_LOCAL_VIEWER_WATCH_SECTIONS = 100
465
466let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
467
468const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
469 MIN: 1,
470 STANDARD: [ 24, 25, 30 ],
471 HD_STANDARD: [ 50, 60 ],
472 AUDIO_MERGE: 25,
473 AVERAGE: 30,
474 MAX: 60,
475 KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum)
476}
477
478const DEFAULT_AUDIO_RESOLUTION = VideoResolution.H_480P
479
480const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {
481 LIKE: 'like',
482 DISLIKE: 'dislike'
483}
484
485const FFMPEG_NICE = {
486 // parent process defaults to niceness = 0
487 // reminder: lower = higher priority, max value is 19, lowest is -20
488 LIVE: 5, // prioritize over VOD and THUMBNAIL
489 THUMBNAIL: 10,
490 VOD: 15
491}
492
493const VIDEO_CATEGORIES = {
494 1: 'Music',
495 2: 'Films',
496 3: 'Vehicles',
497 4: 'Art',
498 5: 'Sports',
499 6: 'Travels',
500 7: 'Gaming',
501 8: 'People',
502 9: 'Comedy',
503 10: 'Entertainment',
504 11: 'News & Politics',
505 12: 'How To',
506 13: 'Education',
507 14: 'Activism',
508 15: 'Science & Technology',
509 16: 'Animals',
510 17: 'Kids',
511 18: 'Food'
512}
513
514// See https://creativecommons.org/licenses/?lang=en
515const VIDEO_LICENCES = {
516 1: 'Attribution',
517 2: 'Attribution - Share Alike',
518 3: 'Attribution - No Derivatives',
519 4: 'Attribution - Non Commercial',
520 5: 'Attribution - Non Commercial - Share Alike',
521 6: 'Attribution - Non Commercial - No Derivatives',
522 7: 'Public Domain Dedication'
523}
524
525const VIDEO_LANGUAGES: { [id: string]: string } = {}
526
527const VIDEO_PRIVACIES: { [ id in VideoPrivacy ]: string } = {
528 [VideoPrivacy.PUBLIC]: 'Public',
529 [VideoPrivacy.UNLISTED]: 'Unlisted',
530 [VideoPrivacy.PRIVATE]: 'Private',
531 [VideoPrivacy.INTERNAL]: 'Internal',
532 [VideoPrivacy.PASSWORD_PROTECTED]: 'Password protected'
533}
534
535const VIDEO_STATES: { [ id in VideoState ]: string } = {
536 [VideoState.PUBLISHED]: 'Published',
537 [VideoState.TO_TRANSCODE]: 'To transcode',
538 [VideoState.TO_IMPORT]: 'To import',
539 [VideoState.WAITING_FOR_LIVE]: 'Waiting for livestream',
540 [VideoState.LIVE_ENDED]: 'Livestream ended',
541 [VideoState.TO_MOVE_TO_EXTERNAL_STORAGE]: 'To move to an external storage',
542 [VideoState.TRANSCODING_FAILED]: 'Transcoding failed',
543 [VideoState.TO_MOVE_TO_EXTERNAL_STORAGE_FAILED]: 'External storage move failed',
544 [VideoState.TO_EDIT]: 'To edit*'
545}
546
547const VIDEO_IMPORT_STATES: { [ id in VideoImportState ]: string } = {
548 [VideoImportState.FAILED]: 'Failed',
549 [VideoImportState.PENDING]: 'Pending',
550 [VideoImportState.SUCCESS]: 'Success',
551 [VideoImportState.REJECTED]: 'Rejected',
552 [VideoImportState.CANCELLED]: 'Cancelled',
553 [VideoImportState.PROCESSING]: 'Processing'
554}
555
556const VIDEO_CHANNEL_SYNC_STATE: { [ id in VideoChannelSyncState ]: string } = {
557 [VideoChannelSyncState.FAILED]: 'Failed',
558 [VideoChannelSyncState.SYNCED]: 'Synchronized',
559 [VideoChannelSyncState.PROCESSING]: 'Processing',
560 [VideoChannelSyncState.WAITING_FIRST_RUN]: 'Waiting first run'
561}
562
563const ABUSE_STATES: { [ id in AbuseState ]: string } = {
564 [AbuseState.PENDING]: 'Pending',
565 [AbuseState.REJECTED]: 'Rejected',
566 [AbuseState.ACCEPTED]: 'Accepted'
567}
568
569const USER_REGISTRATION_STATES: { [ id in UserRegistrationState ]: string } = {
570 [UserRegistrationState.PENDING]: 'Pending',
571 [UserRegistrationState.REJECTED]: 'Rejected',
572 [UserRegistrationState.ACCEPTED]: 'Accepted'
573}
574
575const VIDEO_PLAYLIST_PRIVACIES: { [ id in VideoPlaylistPrivacy ]: string } = {
576 [VideoPlaylistPrivacy.PUBLIC]: 'Public',
577 [VideoPlaylistPrivacy.UNLISTED]: 'Unlisted',
578 [VideoPlaylistPrivacy.PRIVATE]: 'Private'
579}
580
581const VIDEO_PLAYLIST_TYPES: { [ id in VideoPlaylistType ]: string } = {
582 [VideoPlaylistType.REGULAR]: 'Regular',
583 [VideoPlaylistType.WATCH_LATER]: 'Watch later'
584}
585
586const RUNNER_JOB_STATES: { [ id in RunnerJobState ]: string } = {
587 [RunnerJobState.PROCESSING]: 'Processing',
588 [RunnerJobState.COMPLETED]: 'Completed',
589 [RunnerJobState.COMPLETING]: 'Completing',
590 [RunnerJobState.PENDING]: 'Pending',
591 [RunnerJobState.ERRORED]: 'Errored',
592 [RunnerJobState.WAITING_FOR_PARENT_JOB]: 'Waiting for parent job to finish',
593 [RunnerJobState.CANCELLED]: 'Cancelled',
594 [RunnerJobState.PARENT_ERRORED]: 'Parent job failed',
595 [RunnerJobState.PARENT_CANCELLED]: 'Parent job cancelled'
596}
597
598const MIMETYPES = {
599 AUDIO: {
600 MIMETYPE_EXT: {
601 'audio/mpeg': '.mp3',
602 'audio/mp3': '.mp3',
603
604 'application/ogg': '.ogg',
605 'audio/ogg': '.ogg',
606
607 'audio/x-ms-wma': '.wma',
608 'audio/wav': '.wav',
609 'audio/x-wav': '.wav',
610
611 'audio/x-flac': '.flac',
612 'audio/flac': '.flac',
613
614 'audio/vnd.dlna.adts': '.aac',
615 'audio/aac': '.aac',
616
617 'audio/m4a': '.m4a',
618 'audio/mp4': '.m4a',
619 'audio/x-m4a': '.m4a',
620
621 'audio/vnd.dolby.dd-raw': '.ac3',
622 'audio/ac3': '.ac3'
623 },
624 EXT_MIMETYPE: null as { [ id: string ]: string }
625 },
626 VIDEO: {
627 MIMETYPE_EXT: null as { [ id: string ]: string | string[] },
628 MIMETYPES_REGEX: null as string,
629 EXT_MIMETYPE: null as { [ id: string ]: string }
630 },
631 IMAGE: {
632 MIMETYPE_EXT: {
633 'image/png': '.png',
634 'image/gif': '.gif',
635 'image/webp': '.webp',
636 'image/jpg': '.jpg',
637 'image/jpeg': '.jpg'
638 },
639 EXT_MIMETYPE: null as { [ id: string ]: string }
640 },
641 VIDEO_CAPTIONS: {
642 MIMETYPE_EXT: {
643 'text/vtt': '.vtt',
644 'application/x-subrip': '.srt',
645 'text/plain': '.srt'
646 },
647 EXT_MIMETYPE: null as { [ id: string ]: string }
648 },
649 TORRENT: {
650 MIMETYPE_EXT: {
651 'application/x-bittorrent': '.torrent'
652 }
653 },
654 M3U8: {
655 MIMETYPE_EXT: {
656 'application/vnd.apple.mpegurl': '.m3u8'
657 }
658 }
659}
660MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT)
661MIMETYPES.IMAGE.EXT_MIMETYPE = invert(MIMETYPES.IMAGE.MIMETYPE_EXT)
662MIMETYPES.VIDEO_CAPTIONS.EXT_MIMETYPE = invert(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
663
664const BINARY_CONTENT_TYPES = new Set([
665 'binary/octet-stream',
666 'application/octet-stream',
667 'application/x-binary'
668])
669
670// ---------------------------------------------------------------------------
671
672const OVERVIEWS = {
673 VIDEOS: {
674 SAMPLE_THRESHOLD: 6,
675 SAMPLES_COUNT: 20
676 }
677}
678
679// ---------------------------------------------------------------------------
680
681const SERVER_ACTOR_NAME = 'peertube'
682
683const ACTIVITY_PUB = {
684 POTENTIAL_ACCEPT_HEADERS: [
685 'application/activity+json',
686 'application/ld+json',
687 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
688 ],
689 ACCEPT_HEADER: 'application/activity+json, application/ld+json',
690 PUBLIC: 'https://www.w3.org/ns/activitystreams#Public',
691 COLLECTION_ITEMS_PER_PAGE: 10,
692 FETCH_PAGE_LIMIT: 2000,
693 URL_MIME_TYPES: {
694 VIDEO: [] as string[],
695 TORRENT: [ 'application/x-bittorrent' ],
696 MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
697 },
698 MAX_RECURSION_COMMENTS: 100,
699 ACTOR_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2, // 2 days
700 VIDEO_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2, // 2 days
701 VIDEO_PLAYLIST_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2 // 2 days
702}
703
704const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
705 GROUP: 'Group',
706 PERSON: 'Person',
707 APPLICATION: 'Application',
708 ORGANIZATION: 'Organization',
709 SERVICE: 'Service'
710}
711
712const HTTP_SIGNATURE = {
713 HEADER_NAME: 'signature',
714 ALGORITHM: 'rsa-sha256',
715 HEADERS_TO_SIGN_WITH_PAYLOAD: [ '(request-target)', 'host', 'date', 'digest' ],
716 HEADERS_TO_SIGN_WITHOUT_PAYLOAD: [ '(request-target)', 'host', 'date' ],
717 CLOCK_SKEW_SECONDS: 1800
718}
719
720// ---------------------------------------------------------------------------
721
722let PRIVATE_RSA_KEY_SIZE = 2048
723
724// Password encryption
725const BCRYPT_SALT_SIZE = 10
726
727const ENCRYPTION = {
728 ALGORITHM: 'aes-256-cbc',
729 IV: 16,
730 SALT: 'peertube',
731 ENCODING: 'hex' as Encoding
732}
733
734const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes
735const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
736
737const TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME = 60000 * 10 // 10 minutes
738
739const EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
740
741const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
742 DO_NOT_LIST: 'do_not_list',
743 BLUR: 'blur',
744 DISPLAY: 'display'
745}
746
747// ---------------------------------------------------------------------------
748
749// Express static paths (router)
750const STATIC_PATHS = {
751 // TODO: deprecated in v6, to remove
752 THUMBNAILS: '/static/thumbnails/',
753
754 // Need to keep this legacy path for previously generated torrents
755 LEGACY_WEB_VIDEOS: '/static/webseed/',
756 WEB_VIDEOS: '/static/web-videos/',
757
758 // Need to keep this legacy path for previously generated torrents
759 LEGACY_PRIVATE_WEB_VIDEOS: '/static/webseed/private/',
760 PRIVATE_WEB_VIDEOS: '/static/web-videos/private/',
761
762 REDUNDANCY: '/static/redundancy/',
763
764 STREAMING_PLAYLISTS: {
765 HLS: '/static/streaming-playlists/hls',
766 PRIVATE_HLS: '/static/streaming-playlists/hls/private/'
767 }
768}
769const STATIC_DOWNLOAD_PATHS = {
770 TORRENTS: '/download/torrents/',
771 VIDEOS: '/download/videos/',
772 HLS_VIDEOS: '/download/streaming-playlists/hls/videos/'
773}
774const LAZY_STATIC_PATHS = {
775 THUMBNAILS: '/lazy-static/thumbnails/',
776 BANNERS: '/lazy-static/banners/',
777 AVATARS: '/lazy-static/avatars/',
778 PREVIEWS: '/lazy-static/previews/',
779 VIDEO_CAPTIONS: '/lazy-static/video-captions/',
780 TORRENTS: '/lazy-static/torrents/',
781 STORYBOARDS: '/lazy-static/storyboards/'
782}
783const OBJECT_STORAGE_PROXY_PATHS = {
784 // Need to keep this legacy path for previously generated torrents
785 LEGACY_PRIVATE_WEB_VIDEOS: '/object-storage-proxy/webseed/private/',
786 PRIVATE_WEB_VIDEOS: '/object-storage-proxy/web-videos/private/',
787
788 STREAMING_PLAYLISTS: {
789 PRIVATE_HLS: '/object-storage-proxy/streaming-playlists/hls/private/'
790 }
791}
792
793// Cache control
794const STATIC_MAX_AGE = {
795 SERVER: '2h',
796 LAZY_SERVER: '2d',
797 CLIENT: '30d'
798}
799
800// Videos thumbnail size
801const THUMBNAILS_SIZE = {
802 width: 280,
803 height: 157,
804 minWidth: 150
805}
806const PREVIEWS_SIZE = {
807 width: 850,
808 height: 480,
809 minWidth: 400
810}
811const ACTOR_IMAGES_SIZE: { [key in ActorImageType]: { width: number, height: number }[] } = {
812 [ActorImageType.AVATAR]: [
813 {
814 width: 120,
815 height: 120
816 },
817 {
818 width: 48,
819 height: 48
820 }
821 ],
822 [ActorImageType.BANNER]: [
823 {
824 width: 1920,
825 height: 317 // 6/1 ratio
826 }
827 ]
828}
829
830const STORYBOARD = {
831 SPRITE_SIZE: {
832 width: 192,
833 height: 108
834 },
835 SPRITES_MAX_EDGE_COUNT: 10
836}
837
838const EMBED_SIZE = {
839 width: 560,
840 height: 315
841}
842
843// Sub folders of cache directory
844const FILES_CACHE = {
845 PREVIEWS: {
846 DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'),
847 MAX_AGE: 1000 * 3600 * 3 // 3 hours
848 },
849 STORYBOARDS: {
850 DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'storyboards'),
851 MAX_AGE: 1000 * 3600 * 24 // 24 hours
852 },
853 VIDEO_CAPTIONS: {
854 DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions'),
855 MAX_AGE: 1000 * 3600 * 3 // 3 hours
856 },
857 TORRENTS: {
858 DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'torrents'),
859 MAX_AGE: 1000 * 3600 * 3 // 3 hours
860 }
861}
862
863const LRU_CACHE = {
864 USER_TOKENS: {
865 MAX_SIZE: 1000
866 },
867 FILENAME_TO_PATH_PERMANENT_FILE_CACHE: {
868 MAX_SIZE: 1000
869 },
870 STATIC_VIDEO_FILES_RIGHTS_CHECK: {
871 MAX_SIZE: 5000,
872 TTL: parseDurationToMs('10 seconds')
873 },
874 VIDEO_TOKENS: {
875 MAX_SIZE: 100_000,
876 TTL: parseDurationToMs('8 hours')
877 },
878 TRACKER_IPS: {
879 MAX_SIZE: 100_000
880 }
881}
882
883const DIRECTORIES = {
884 RESUMABLE_UPLOAD: join(CONFIG.STORAGE.TMP_DIR, 'resumable-uploads'),
885
886 HLS_STREAMING_PLAYLIST: {
887 PUBLIC: join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls'),
888 PRIVATE: join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls', 'private')
889 },
890
891 VIDEOS: {
892 PUBLIC: CONFIG.STORAGE.WEB_VIDEOS_DIR,
893 PRIVATE: join(CONFIG.STORAGE.WEB_VIDEOS_DIR, 'private')
894 },
895
896 HLS_REDUNDANCY: join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls')
897}
898
899const RESUMABLE_UPLOAD_SESSION_LIFETIME = SCHEDULER_INTERVALS_MS.REMOVE_DANGLING_RESUMABLE_UPLOADS
900
901const VIDEO_LIVE = {
902 EXTENSION: '.ts',
903 CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes
904 SEGMENT_TIME_SECONDS: {
905 DEFAULT_LATENCY: 4, // 4 seconds
906 SMALL_LATENCY: 2 // 2 seconds
907 },
908 SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist
909 REPLAY_DIRECTORY: 'replay',
910 EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4,
911 MAX_SOCKET_WAITING_DATA: 1024 * 1000 * 100, // 100MB
912 RTMP: {
913 CHUNK_SIZE: 60000,
914 GOP_CACHE: true,
915 PING: 60,
916 PING_TIMEOUT: 30,
917 BASE_PATH: 'live'
918 }
919}
920
921const MEMOIZE_TTL = {
922 OVERVIEWS_SAMPLE: 1000 * 3600 * 4, // 4 hours
923 INFO_HASH_EXISTS: 1000 * 60, // 1 minute
924 VIDEO_DURATION: 1000 * 10, // 10 seconds
925 LIVE_ABLE_TO_UPLOAD: 1000 * 60, // 1 minute
926 LIVE_CHECK_SOCKET_HEALTH: 1000 * 60, // 1 minute
927 GET_STATS_FOR_OPEN_TELEMETRY_METRICS: 1000 * 60 // 1 minute
928}
929
930const MEMOIZE_LENGTH = {
931 INFO_HASH_EXISTS: 200,
932 VIDEO_DURATION: 200
933}
934
935const WORKER_THREADS = {
936 DOWNLOAD_IMAGE: {
937 CONCURRENCY: 3,
938 MAX_THREADS: 1
939 },
940 PROCESS_IMAGE: {
941 CONCURRENCY: 1,
942 MAX_THREADS: 5
943 }
944}
945
946const REDUNDANCY = {
947 VIDEOS: {
948 RANDOMIZED_FACTOR: 5
949 }
950}
951
952const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS)
953const OTP = {
954 HEADER_NAME: 'x-peertube-otp',
955 HEADER_REQUIRED_VALUE: 'required; app'
956}
957
958const ASSETS_PATH = {
959 DEFAULT_AUDIO_BACKGROUND: join(root(), 'dist', 'server', 'assets', 'default-audio-background.jpg'),
960 DEFAULT_LIVE_BACKGROUND: join(root(), 'dist', 'server', 'assets', 'default-live-background.jpg')
961}
962
963// ---------------------------------------------------------------------------
964
965const CUSTOM_HTML_TAG_COMMENTS = {
966 TITLE: '<!-- title tag -->',
967 DESCRIPTION: '<!-- description tag -->',
968 CUSTOM_CSS: '<!-- custom css tag -->',
969 META_TAGS: '<!-- meta tags -->',
970 SERVER_CONFIG: '<!-- server config -->'
971}
972
973const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000
974const LOG_FILENAME = 'peertube.log'
975const AUDIT_LOG_FILENAME = 'peertube-audit.log'
976
977// ---------------------------------------------------------------------------
978
979const TRACKER_RATE_LIMITS = {
980 INTERVAL: 60000 * 5, // 5 minutes
981 ANNOUNCES_PER_IP_PER_INFOHASH: 15, // maximum announces per torrent in the interval
982 ANNOUNCES_PER_IP: 30, // maximum announces for all our torrents in the interval
983 BLOCK_IP_LIFETIME: parseDurationToMs('3 minutes')
984}
985
986const P2P_MEDIA_LOADER_PEER_VERSION = 2
987
988// ---------------------------------------------------------------------------
989
990const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css'
991const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME)
992
993let PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 1000 * 60 * 5 // 5 minutes
994
995const DEFAULT_THEME_NAME = 'default'
996const DEFAULT_USER_THEME_NAME = 'instance-default'
997
998// ---------------------------------------------------------------------------
999
1000const SEARCH_INDEX = {
1001 ROUTES: {
1002 VIDEOS: '/api/v1/search/videos',
1003 VIDEO_CHANNELS: '/api/v1/search/video-channels'
1004 }
1005}
1006
1007// ---------------------------------------------------------------------------
1008
1009const STATS_TIMESERIE = {
1010 MAX_DAYS: 365 * 10 // Around 10 years
1011}
1012
1013// ---------------------------------------------------------------------------
1014
1015// Special constants for a test instance
1016if (process.env.PRODUCTION_CONSTANTS !== 'true') {
1017 if (isTestOrDevInstance()) {
1018 PRIVATE_RSA_KEY_SIZE = 1024
1019
1020 ACTOR_FOLLOW_SCORE.BASE = 20
1021
1022 REMOTE_SCHEME.HTTP = 'http'
1023 REMOTE_SCHEME.WS = 'ws'
1024
1025 STATIC_MAX_AGE.SERVER = '0'
1026
1027 SCHEDULER_INTERVALS_MS.ACTOR_FOLLOW_SCORES = 1000
1028 SCHEDULER_INTERVALS_MS.REMOVE_OLD_JOBS = 10000
1029 SCHEDULER_INTERVALS_MS.REMOVE_OLD_HISTORY = 5000
1030 SCHEDULER_INTERVALS_MS.REMOVE_OLD_VIEWS = 5000
1031 SCHEDULER_INTERVALS_MS.UPDATE_VIDEOS = 5000
1032 SCHEDULER_INTERVALS_MS.AUTO_FOLLOW_INDEX_INSTANCES = 5000
1033 SCHEDULER_INTERVALS_MS.UPDATE_INBOX_STATS = 5000
1034 SCHEDULER_INTERVALS_MS.CHECK_PEERTUBE_VERSION = 2000
1035
1036 REPEAT_JOBS['videos-views-stats'] = { every: 5000 }
1037
1038 REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 }
1039 AP_CLEANER.PERIOD = 5000
1040
1041 REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1
1042
1043 CONTACT_FORM_LIFETIME = 1000 // 1 second
1044
1045 JOB_ATTEMPTS['email'] = 1
1046
1047 FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
1048 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 3000
1049 MEMOIZE_TTL.LIVE_ABLE_TO_UPLOAD = 3000
1050 OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2
1051
1052 PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000
1053
1054 JOB_REMOVAL_OPTIONS.SUCCESS['videos-views-stats'] = 10000
1055 }
1056
1057 if (isTestInstance()) {
1058 ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2
1059 ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
1060 ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
1061 ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
1062
1063 CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max = 100 * 1024 // 100KB
1064 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max = 400 * 1024 // 400KB
1065
1066 VIEW_LIFETIME.VIEWER_COUNTER = 1000 * 5 // 5 second
1067 VIEW_LIFETIME.VIEWER_STATS = 1000 * 5 // 5 second
1068
1069 VIDEO_LIVE.CLEANUP_DELAY = getIntEnv('PEERTUBE_TEST_CONSTANTS_VIDEO_LIVE_CLEANUP_DELAY') ?? 5000
1070 VIDEO_LIVE.SEGMENT_TIME_SECONDS.DEFAULT_LATENCY = 2
1071 VIDEO_LIVE.SEGMENT_TIME_SECONDS.SMALL_LATENCY = 1
1072 VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION = 1
1073
1074 RUNNER_JOBS.LAST_CONTACT_UPDATE_INTERVAL = 2000
1075 }
1076}
1077
1078updateWebserverUrls()
1079updateWebserverConfig()
1080
1081registerConfigChangedHandler(() => {
1082 updateWebserverUrls()
1083 updateWebserverConfig()
1084})
1085
1086// ---------------------------------------------------------------------------
1087
1088const FILES_CONTENT_HASH = {
1089 MANIFEST: generateContentHash(),
1090 FAVICON: generateContentHash(),
1091 LOGO: generateContentHash()
1092}
1093
1094// ---------------------------------------------------------------------------
1095
1096const VIDEO_FILTERS = {
1097 WATERMARK: {
1098 SIZE_RATIO: 1 / 10,
1099 HORIZONTAL_MARGIN_RATIO: 1 / 20,
1100 VERTICAL_MARGIN_RATIO: 1 / 20
1101 }
1102}
1103
1104// ---------------------------------------------------------------------------
1105
1106export {
1107 WEBSERVER,
1108 API_VERSION,
1109 ENCRYPTION,
1110 VIDEO_LIVE,
1111 PEERTUBE_VERSION,
1112 LAZY_STATIC_PATHS,
1113 OBJECT_STORAGE_PROXY_PATHS,
1114 SEARCH_INDEX,
1115 DIRECTORIES,
1116 RESUMABLE_UPLOAD_SESSION_LIFETIME,
1117 RUNNER_JOB_STATES,
1118 P2P_MEDIA_LOADER_PEER_VERSION,
1119 STORYBOARD,
1120 ACTOR_IMAGES_SIZE,
1121 ACCEPT_HEADERS,
1122 BCRYPT_SALT_SIZE,
1123 TRACKER_RATE_LIMITS,
1124 FILES_CACHE,
1125 LOG_FILENAME,
1126 CONSTRAINTS_FIELDS,
1127 EMBED_SIZE,
1128 REDUNDANCY,
1129 JOB_CONCURRENCY,
1130 JOB_ATTEMPTS,
1131 AP_CLEANER,
1132 LAST_MIGRATION_VERSION,
1133 CUSTOM_HTML_TAG_COMMENTS,
1134 STATS_TIMESERIE,
1135 BROADCAST_CONCURRENCY,
1136 AUDIT_LOG_FILENAME,
1137 PAGINATION,
1138 ACTOR_FOLLOW_SCORE,
1139 PREVIEWS_SIZE,
1140 REMOTE_SCHEME,
1141 FOLLOW_STATES,
1142 DEFAULT_USER_THEME_NAME,
1143 SERVER_ACTOR_NAME,
1144 TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME,
1145 PLUGIN_GLOBAL_CSS_FILE_NAME,
1146 PLUGIN_GLOBAL_CSS_PATH,
1147 PRIVATE_RSA_KEY_SIZE,
1148 VIDEO_FILTERS,
1149 ROUTE_CACHE_LIFETIME,
1150 SORTABLE_COLUMNS,
1151 JOB_TTL,
1152 DEFAULT_THEME_NAME,
1153 NSFW_POLICY_TYPES,
1154 STATIC_MAX_AGE,
1155 STATIC_PATHS,
1156 VIDEO_IMPORT_TIMEOUT,
1157 VIDEO_PLAYLIST_TYPES,
1158 MAX_LOGS_OUTPUT_CHARACTERS,
1159 ACTIVITY_PUB,
1160 ACTIVITY_PUB_ACTOR_TYPES,
1161 THUMBNAILS_SIZE,
1162 VIDEO_CATEGORIES,
1163 MEMOIZE_LENGTH,
1164 VIDEO_LANGUAGES,
1165 VIDEO_PRIVACIES,
1166 VIDEO_LICENCES,
1167 VIDEO_STATES,
1168 WORKER_THREADS,
1169 VIDEO_RATE_TYPES,
1170 JOB_PRIORITY,
1171 VIDEO_TRANSCODING_FPS,
1172 FFMPEG_NICE,
1173 ABUSE_STATES,
1174 USER_REGISTRATION_STATES,
1175 LRU_CACHE,
1176 REQUEST_TIMEOUTS,
1177 RUNNER_JOBS,
1178 MAX_LOCAL_VIEWER_WATCH_SECTIONS,
1179 USER_PASSWORD_RESET_LIFETIME,
1180 USER_PASSWORD_CREATE_LIFETIME,
1181 MEMOIZE_TTL,
1182 EMAIL_VERIFY_LIFETIME,
1183 OVERVIEWS,
1184 SCHEDULER_INTERVALS_MS,
1185 REPEAT_JOBS,
1186 STATIC_DOWNLOAD_PATHS,
1187 MIMETYPES,
1188 CRAWL_REQUEST_CONCURRENCY,
1189 DEFAULT_AUDIO_RESOLUTION,
1190 BINARY_CONTENT_TYPES,
1191 JOB_REMOVAL_OPTIONS,
1192 HTTP_SIGNATURE,
1193 VIDEO_IMPORT_STATES,
1194 VIDEO_CHANNEL_SYNC_STATE,
1195 VIEW_LIFETIME,
1196 CONTACT_FORM_LIFETIME,
1197 VIDEO_PLAYLIST_PRIVACIES,
1198 PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME,
1199 ASSETS_PATH,
1200 FILES_CONTENT_HASH,
1201 OTP,
1202 loadLanguages,
1203 buildLanguages,
1204 generateContentHash
1205}
1206
1207// ---------------------------------------------------------------------------
1208
1209function buildVideoMimetypeExt () {
1210 const data = {
1211 // streamable formats that warrant cross-browser compatibility
1212 'video/webm': '.webm',
1213 // We'll add .ogg if additional extensions are enabled
1214 // We could add .ogg here but since it could be an audio file,
1215 // it would be confusing for users because PeerTube will refuse their file (based on the mimetype)
1216 'video/ogg': [ '.ogv' ],
1217 'video/mp4': '.mp4'
1218 }
1219
1220 if (CONFIG.TRANSCODING.ENABLED) {
1221 if (CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) {
1222 data['video/ogg'].push('.ogg')
1223
1224 Object.assign(data, {
1225 'video/x-matroska': '.mkv',
1226
1227 // Developed by Apple
1228 'video/quicktime': [ '.mov', '.qt', '.mqv' ], // often used as output format by editing software
1229 'video/x-m4v': '.m4v',
1230 'video/m4v': '.m4v',
1231
1232 // Developed by the Adobe Flash Platform
1233 'video/x-flv': '.flv',
1234 'video/x-f4v': '.f4v', // replacement for flv
1235
1236 // Developed by Microsoft
1237 'video/x-ms-wmv': '.wmv',
1238 'video/x-msvideo': '.avi',
1239 'video/avi': '.avi',
1240
1241 // Developed by 3GPP
1242 // common video formats for cell phones
1243 'video/3gpp': [ '.3gp', '.3gpp' ],
1244 'video/3gpp2': [ '.3g2', '.3gpp2' ],
1245
1246 // Developed by FFmpeg/Mplayer
1247 'application/x-nut': '.nut',
1248
1249 // The standard video format used by many Sony and Panasonic HD camcorders.
1250 // It is also used for storing high definition video on Blu-ray discs.
1251 'video/mp2t': '.mts',
1252 'video/vnd.dlna.mpeg-tts': '.mts',
1253
1254 'video/m2ts': '.m2ts',
1255
1256 // Old formats reliant on MPEG-1/MPEG-2
1257 'video/mpv': '.mpv',
1258 'video/mpeg2': '.m2v',
1259 'video/mpeg': [ '.m1v', '.mpg', '.mpe', '.mpeg', '.vob' ],
1260 'video/dvd': '.vob',
1261
1262 // Could be anything
1263 'application/octet-stream': null,
1264 'application/mxf': '.mxf' // often used as exchange format by editing software
1265 })
1266 }
1267
1268 if (CONFIG.TRANSCODING.ALLOW_AUDIO_FILES) {
1269 Object.assign(data, MIMETYPES.AUDIO.MIMETYPE_EXT)
1270 }
1271 }
1272
1273 return data
1274}
1275
1276function updateWebserverUrls () {
1277 WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT)
1278 WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP)
1279 WEBSERVER.WS = CONFIG.WEBSERVER.WS
1280
1281 WEBSERVER.SCHEME = CONFIG.WEBSERVER.SCHEME
1282 WEBSERVER.HOSTNAME = CONFIG.WEBSERVER.HOSTNAME
1283 WEBSERVER.PORT = CONFIG.WEBSERVER.PORT
1284
1285 const rtmpHostname = CONFIG.LIVE.RTMP.PUBLIC_HOSTNAME || CONFIG.WEBSERVER.HOSTNAME
1286 const rtmpsHostname = CONFIG.LIVE.RTMPS.PUBLIC_HOSTNAME || CONFIG.WEBSERVER.HOSTNAME
1287
1288 WEBSERVER.RTMP_URL = 'rtmp://' + rtmpHostname + ':' + CONFIG.LIVE.RTMP.PORT
1289 WEBSERVER.RTMPS_URL = 'rtmps://' + rtmpsHostname + ':' + CONFIG.LIVE.RTMPS.PORT
1290
1291 WEBSERVER.RTMP_BASE_LIVE_URL = WEBSERVER.RTMP_URL + '/' + VIDEO_LIVE.RTMP.BASE_PATH
1292 WEBSERVER.RTMPS_BASE_LIVE_URL = WEBSERVER.RTMPS_URL + '/' + VIDEO_LIVE.RTMP.BASE_PATH
1293}
1294
1295function updateWebserverConfig () {
1296 MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt()
1297 MIMETYPES.VIDEO.MIMETYPES_REGEX = buildMimetypesRegex(MIMETYPES.VIDEO.MIMETYPE_EXT)
1298
1299 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
1300
1301 MIMETYPES.VIDEO.EXT_MIMETYPE = buildVideoExtMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT)
1302
1303 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = Object.keys(MIMETYPES.VIDEO.EXT_MIMETYPE)
1304}
1305
1306function buildVideoExtMimetype (obj: { [ id: string ]: string | string[] }) {
1307 const result: { [id: string]: string } = {}
1308
1309 for (const mimetype of Object.keys(obj)) {
1310 const value = obj[mimetype]
1311 if (!value) continue
1312
1313 const extensions = Array.isArray(value) ? value : [ value ]
1314
1315 for (const extension of extensions) {
1316 result[extension] = mimetype
1317 }
1318 }
1319
1320 return result
1321}
1322
1323function buildMimetypesRegex (obj: { [id: string]: string | string[] }) {
1324 return Object.keys(obj)
1325 .map(m => `(${m})`)
1326 .join('|')
1327}
1328
1329function loadLanguages () {
1330 Object.assign(VIDEO_LANGUAGES, buildLanguages())
1331}
1332
1333function buildLanguages () {
1334 const iso639 = require('iso-639-3')
1335
1336 const languages: { [id: string]: string } = {}
1337
1338 const additionalLanguages = {
1339 sgn: true, // Sign languages (macro language)
1340 ase: true, // American sign language
1341 asq: true, // Austrian sign language
1342 sdl: true, // Arabian sign language
1343 bfi: true, // British sign language
1344 bzs: true, // Brazilian sign language
1345 csl: true, // Chinese sign language
1346 cse: true, // Czech sign language
1347 dsl: true, // Danish sign language
1348 fsl: true, // French sign language
1349 gsg: true, // German sign language
1350 pks: true, // Pakistan sign language
1351 jsl: true, // Japanese sign language
1352 sfs: true, // South African sign language
1353 swl: true, // Swedish sign language
1354 rsl: true, // Russian sign language
1355
1356 kab: true, // Kabyle
1357
1358 lat: true, // Latin
1359
1360 epo: true, // Esperanto
1361 tlh: true, // Klingon
1362 jbo: true, // Lojban
1363 avk: true, // Kotava
1364
1365 zxx: true // No linguistic content (ISO-639-2)
1366 }
1367
1368 // Only add ISO639-1 languages and some sign languages (ISO639-3)
1369 iso639
1370 .filter(l => {
1371 return (l.iso6391 !== undefined && l.type === 'living') ||
1372 additionalLanguages[l.iso6393] === true
1373 })
1374 .forEach(l => { languages[l.iso6391 || l.iso6393] = l.name })
1375
1376 // Override Occitan label
1377 languages['oc'] = 'Occitan'
1378 languages['el'] = 'Greek'
1379 languages['tok'] = 'Toki Pona'
1380
1381 // Chinese languages
1382 languages['zh-Hans'] = 'Simplified Chinese'
1383 languages['zh-Hant'] = 'Traditional Chinese'
1384
1385 return languages
1386}
1387
1388function generateContentHash () {
1389 return randomBytes(20).toString('hex')
1390}
1391
1392function getIntEnv (path: string) {
1393 if (process.env[path]) return parseInt(process.env[path])
1394
1395 return undefined
1396}
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
deleted file mode 100644
index bc120e398..000000000
--- a/server/initializers/database.ts
+++ /dev/null
@@ -1,233 +0,0 @@
1import { QueryTypes, Transaction } from 'sequelize'
2import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
3import { ActorCustomPageModel } from '@server/models/account/actor-custom-page'
4import { RunnerModel } from '@server/models/runner/runner'
5import { RunnerJobModel } from '@server/models/runner/runner-job'
6import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token'
7import { TrackerModel } from '@server/models/server/tracker'
8import { VideoTrackerModel } from '@server/models/server/video-tracker'
9import { UserModel } from '@server/models/user/user'
10import { UserNotificationModel } from '@server/models/user/user-notification'
11import { UserRegistrationModel } from '@server/models/user/user-registration'
12import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
13import { StoryboardModel } from '@server/models/video/storyboard'
14import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
15import { VideoJobInfoModel } from '@server/models/video/video-job-info'
16import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
17import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
18import { VideoSourceModel } from '@server/models/video/video-source'
19import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
20import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section'
21import { isTestOrDevInstance } from '../helpers/core-utils'
22import { logger } from '../helpers/logger'
23import { AbuseModel } from '../models/abuse/abuse'
24import { AbuseMessageModel } from '../models/abuse/abuse-message'
25import { VideoAbuseModel } from '../models/abuse/video-abuse'
26import { VideoCommentAbuseModel } from '../models/abuse/video-comment-abuse'
27import { AccountModel } from '../models/account/account'
28import { AccountBlocklistModel } from '../models/account/account-blocklist'
29import { AccountVideoRateModel } from '../models/account/account-video-rate'
30import { ActorModel } from '../models/actor/actor'
31import { ActorFollowModel } from '../models/actor/actor-follow'
32import { ActorImageModel } from '../models/actor/actor-image'
33import { ApplicationModel } from '../models/application/application'
34import { OAuthClientModel } from '../models/oauth/oauth-client'
35import { OAuthTokenModel } from '../models/oauth/oauth-token'
36import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
37import { PluginModel } from '../models/server/plugin'
38import { ServerModel } from '../models/server/server'
39import { ServerBlocklistModel } from '../models/server/server-blocklist'
40import { UserNotificationSettingModel } from '../models/user/user-notification-setting'
41import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
42import { TagModel } from '../models/video/tag'
43import { ThumbnailModel } from '../models/video/thumbnail'
44import { VideoModel } from '../models/video/video'
45import { VideoBlacklistModel } from '../models/video/video-blacklist'
46import { VideoCaptionModel } from '../models/video/video-caption'
47import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
48import { VideoChannelModel } from '../models/video/video-channel'
49import { VideoCommentModel } from '../models/video/video-comment'
50import { VideoFileModel } from '../models/video/video-file'
51import { VideoImportModel } from '../models/video/video-import'
52import { VideoLiveModel } from '../models/video/video-live'
53import { VideoPlaylistModel } from '../models/video/video-playlist'
54import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
55import { VideoShareModel } from '../models/video/video-share'
56import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
57import { VideoTagModel } from '../models/video/video-tag'
58import { VideoViewModel } from '../models/view/video-view'
59import { CONFIG } from './config'
60import { VideoPasswordModel } from '@server/models/video/video-password'
61
62require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
63
64const dbname = CONFIG.DATABASE.DBNAME
65const username = CONFIG.DATABASE.USERNAME
66const password = CONFIG.DATABASE.PASSWORD
67const host = CONFIG.DATABASE.HOSTNAME
68const port = CONFIG.DATABASE.PORT
69const poolMax = CONFIG.DATABASE.POOL.MAX
70
71let dialectOptions: any = {}
72
73if (CONFIG.DATABASE.SSL) {
74 dialectOptions = {
75 ssl: {
76 rejectUnauthorized: false
77 }
78 }
79}
80
81const sequelizeTypescript = new SequelizeTypescript({
82 database: dbname,
83 dialect: 'postgres',
84 dialectOptions,
85 host,
86 port,
87 username,
88 password,
89 pool: {
90 max: poolMax
91 },
92 benchmark: isTestOrDevInstance(),
93 isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE,
94 logging: (message: string, benchmark: number) => {
95 if (process.env.NODE_DB_LOG === 'false') return
96
97 let newMessage = 'Executed SQL request'
98 if (isTestOrDevInstance() === true && benchmark !== undefined) {
99 newMessage += ' in ' + benchmark + 'ms'
100 }
101
102 logger.debug(newMessage, { sql: message, tags: [ 'sql' ] })
103 }
104})
105
106function checkDatabaseConnectionOrDie () {
107 sequelizeTypescript.authenticate()
108 .then(() => logger.debug('Connection to PostgreSQL has been established successfully.'))
109 .catch(err => {
110
111 logger.error('Unable to connect to PostgreSQL database.', { err })
112 process.exit(-1)
113 })
114}
115
116async function initDatabaseModels (silent: boolean) {
117 sequelizeTypescript.addModels([
118 ApplicationModel,
119 ActorModel,
120 ActorFollowModel,
121 ActorImageModel,
122 AccountModel,
123 OAuthClientModel,
124 OAuthTokenModel,
125 ServerModel,
126 TagModel,
127 AccountVideoRateModel,
128 UserModel,
129 AbuseMessageModel,
130 AbuseModel,
131 VideoCommentAbuseModel,
132 VideoAbuseModel,
133 VideoModel,
134 VideoChangeOwnershipModel,
135 VideoChannelModel,
136 VideoShareModel,
137 VideoFileModel,
138 VideoSourceModel,
139 VideoCaptionModel,
140 VideoBlacklistModel,
141 VideoTagModel,
142 VideoCommentModel,
143 ScheduleVideoUpdateModel,
144 VideoImportModel,
145 VideoViewModel,
146 VideoRedundancyModel,
147 UserVideoHistoryModel,
148 VideoLiveModel,
149 VideoLiveSessionModel,
150 VideoLiveReplaySettingModel,
151 AccountBlocklistModel,
152 ServerBlocklistModel,
153 UserNotificationModel,
154 UserNotificationSettingModel,
155 VideoStreamingPlaylistModel,
156 VideoPlaylistModel,
157 VideoPlaylistElementModel,
158 LocalVideoViewerModel,
159 LocalVideoViewerWatchSectionModel,
160 ThumbnailModel,
161 TrackerModel,
162 VideoTrackerModel,
163 PluginModel,
164 ActorCustomPageModel,
165 VideoJobInfoModel,
166 VideoChannelSyncModel,
167 UserRegistrationModel,
168 VideoPasswordModel,
169 RunnerRegistrationTokenModel,
170 RunnerModel,
171 RunnerJobModel,
172 StoryboardModel
173 ])
174
175 // Check extensions exist in the database
176 await checkPostgresExtensions()
177
178 // Create custom PostgreSQL functions
179 await createFunctions()
180
181 if (!silent) logger.info('Database %s is ready.', dbname)
182}
183
184// ---------------------------------------------------------------------------
185
186export {
187 initDatabaseModels,
188 checkDatabaseConnectionOrDie,
189 sequelizeTypescript
190}
191
192// ---------------------------------------------------------------------------
193
194async function checkPostgresExtensions () {
195 const promises = [
196 checkPostgresExtension('pg_trgm'),
197 checkPostgresExtension('unaccent')
198 ]
199
200 return Promise.all(promises)
201}
202
203async function checkPostgresExtension (extension: string) {
204 const query = `SELECT 1 FROM pg_available_extensions WHERE name = '${extension}' AND installed_version IS NOT NULL;`
205 const options = {
206 type: QueryTypes.SELECT as QueryTypes.SELECT,
207 raw: true
208 }
209
210 const res = await sequelizeTypescript.query<object>(query, options)
211
212 if (!res || res.length === 0) {
213 // Try to create the extension ourselves
214 try {
215 await sequelizeTypescript.query(`CREATE EXTENSION ${extension};`, { raw: true })
216
217 } catch {
218 const errorMessage = `You need to enable ${extension} extension in PostgreSQL. ` +
219 `You can do so by running 'CREATE EXTENSION ${extension};' as a PostgreSQL super user in ${CONFIG.DATABASE.DBNAME} database.`
220 throw new Error(errorMessage)
221 }
222 }
223}
224
225function createFunctions () {
226 const query = `CREATE OR REPLACE FUNCTION immutable_unaccent(text)
227 RETURNS text AS
228$func$
229SELECT public.unaccent('public.unaccent', $1::text)
230$func$ LANGUAGE sql IMMUTABLE;`
231
232 return sequelizeTypescript.query(query, { raw: true })
233}
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
deleted file mode 100644
index 2406a5936..000000000
--- a/server/initializers/installer.ts
+++ /dev/null
@@ -1,199 +0,0 @@
1import { ensureDir, readdir, remove } from 'fs-extra'
2import passwordGenerator from 'password-generator'
3import { join } from 'path'
4import { isTestOrDevInstance } from '@server/helpers/core-utils'
5import { generateRunnerRegistrationToken } from '@server/helpers/token-generator'
6import { getNodeABIVersion } from '@server/helpers/version'
7import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token'
8import { UserRole } from '@shared/models'
9import { logger } from '../helpers/logger'
10import { buildUser, createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user'
11import { ApplicationModel } from '../models/application/application'
12import { OAuthClientModel } from '../models/oauth/oauth-client'
13import { applicationExist, clientsExist, usersExist } from './checker-after-init'
14import { CONFIG } from './config'
15import { DIRECTORIES, FILES_CACHE, LAST_MIGRATION_VERSION } from './constants'
16import { sequelizeTypescript } from './database'
17
18async function installApplication () {
19 try {
20 await Promise.all([
21 // Database related
22 sequelizeTypescript.sync()
23 .then(() => {
24 return Promise.all([
25 createApplicationIfNotExist(),
26 createOAuthClientIfNotExist(),
27 createOAuthAdminIfNotExist(),
28 createRunnerRegistrationTokenIfNotExist()
29 ])
30 }),
31
32 // Directories
33 removeCacheAndTmpDirectories()
34 .then(() => createDirectoriesIfNotExist())
35 ])
36 } catch (err) {
37 logger.error('Cannot install application.', { err })
38 process.exit(-1)
39 }
40}
41
42// ---------------------------------------------------------------------------
43
44export {
45 installApplication
46}
47
48// ---------------------------------------------------------------------------
49
50function removeCacheAndTmpDirectories () {
51 const cacheDirectories = Object.keys(FILES_CACHE)
52 .map(k => FILES_CACHE[k].DIRECTORY)
53
54 const tasks: Promise<any>[] = []
55
56 // Cache directories
57 for (const dir of cacheDirectories) {
58 tasks.push(removeDirectoryOrContent(dir))
59 }
60
61 tasks.push(removeDirectoryOrContent(CONFIG.STORAGE.TMP_DIR))
62
63 return Promise.all(tasks)
64}
65
66async function removeDirectoryOrContent (dir: string) {
67 try {
68 await remove(dir)
69 } catch (err) {
70 logger.debug('Cannot remove directory %s. Removing content instead.', dir, { err })
71
72 const files = await readdir(dir)
73
74 for (const file of files) {
75 await remove(join(dir, file))
76 }
77 }
78}
79
80function createDirectoriesIfNotExist () {
81 const storage = CONFIG.STORAGE
82 const cacheDirectories = Object.keys(FILES_CACHE)
83 .map(k => FILES_CACHE[k].DIRECTORY)
84
85 const tasks: Promise<void>[] = []
86 for (const key of Object.keys(storage)) {
87 const dir = storage[key]
88 tasks.push(ensureDir(dir))
89 }
90
91 // Cache directories
92 for (const dir of cacheDirectories) {
93 tasks.push(ensureDir(dir))
94 }
95
96 tasks.push(ensureDir(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE))
97 tasks.push(ensureDir(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC))
98 tasks.push(ensureDir(DIRECTORIES.VIDEOS.PUBLIC))
99 tasks.push(ensureDir(DIRECTORIES.VIDEOS.PRIVATE))
100
101 // Resumable upload directory
102 tasks.push(ensureDir(DIRECTORIES.RESUMABLE_UPLOAD))
103
104 return Promise.all(tasks)
105}
106
107async function createOAuthClientIfNotExist () {
108 const exist = await clientsExist()
109 // Nothing to do, clients already exist
110 if (exist === true) return undefined
111
112 logger.info('Creating a default OAuth Client.')
113
114 const id = passwordGenerator(32, false, /[a-z0-9]/)
115 const secret = passwordGenerator(32, false, /[a-zA-Z0-9]/)
116 const client = new OAuthClientModel({
117 clientId: id,
118 clientSecret: secret,
119 grants: [ 'password', 'refresh_token' ],
120 redirectUris: null
121 })
122
123 const createdClient = await client.save()
124 logger.info('Client id: ' + createdClient.clientId)
125 logger.info('Client secret: ' + createdClient.clientSecret)
126
127 return undefined
128}
129
130async function createOAuthAdminIfNotExist () {
131 const exist = await usersExist()
132 // Nothing to do, users already exist
133 if (exist === true) return undefined
134
135 logger.info('Creating the administrator.')
136
137 const username = 'root'
138 const role = UserRole.ADMINISTRATOR
139 const email = CONFIG.ADMIN.EMAIL
140 let validatePassword = true
141 let password = ''
142
143 // Do not generate a random password for test and dev environments
144 if (isTestOrDevInstance()) {
145 password = 'test'
146
147 if (process.env.NODE_APP_INSTANCE) {
148 password += process.env.NODE_APP_INSTANCE
149 }
150
151 // Our password is weak so do not validate it
152 validatePassword = false
153 } else if (process.env.PT_INITIAL_ROOT_PASSWORD) {
154 password = process.env.PT_INITIAL_ROOT_PASSWORD
155 } else {
156 password = passwordGenerator(16, true)
157 }
158
159 const user = buildUser({
160 username,
161 email,
162 password,
163 role,
164 emailVerified: true,
165 videoQuota: -1,
166 videoQuotaDaily: -1
167 })
168
169 await createUserAccountAndChannelAndPlaylist({ userToCreate: user, channelNames: undefined, validateUser: validatePassword })
170 logger.info('Username: ' + username)
171 logger.info('User password: ' + password)
172}
173
174async function createApplicationIfNotExist () {
175 const exist = await applicationExist()
176 // Nothing to do, application already exist
177 if (exist === true) return undefined
178
179 logger.info('Creating application account.')
180
181 const application = await ApplicationModel.create({
182 migrationVersion: LAST_MIGRATION_VERSION,
183 nodeVersion: process.version,
184 nodeABIVersion: getNodeABIVersion()
185 })
186
187 return createApplicationActor(application.id)
188}
189
190async function createRunnerRegistrationTokenIfNotExist () {
191 const total = await RunnerRegistrationTokenModel.countTotal()
192 if (total !== 0) return undefined
193
194 const token = new RunnerRegistrationTokenModel({
195 registrationToken: generateRunnerRegistrationToken()
196 })
197
198 await token.save()
199}
diff --git a/server/initializers/migrations/0505-user-last-login-date.ts b/server/initializers/migrations/0505-user-last-login-date.ts
deleted file mode 100644
index 29d970802..000000000
--- a/server/initializers/migrations/0505-user-last-login-date.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8
9 {
10 const field = {
11 type: Sequelize.DATE,
12 allowNull: true
13 }
14 await utils.queryInterface.addColumn('user', 'lastLoginDate', field)
15 }
16
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/initializers/migrations/0510-video-file-metadata.ts b/server/initializers/migrations/0510-video-file-metadata.ts
deleted file mode 100644
index be9feb47a..000000000
--- a/server/initializers/migrations/0510-video-file-metadata.ts
+++ /dev/null
@@ -1,38 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8
9 // We made a mistake with the migration in 2.2.0-rc.1
10 // Docker containers did not include this migration file
11 // So we check the table definition and add the column if it does not exist
12 const tableDefinition = await utils.queryInterface.describeTable('videoFile')
13
14 if (!tableDefinition['metadata']) {
15 const metadata = {
16 type: Sequelize.JSONB,
17 allowNull: true
18 }
19 await utils.queryInterface.addColumn('videoFile', 'metadata', metadata)
20 }
21
22 if (!tableDefinition['metadataUrl']) {
23 const metadataUrl = {
24 type: Sequelize.STRING,
25 allowNull: true
26 }
27 await utils.queryInterface.addColumn('videoFile', 'metadataUrl', metadataUrl)
28 }
29}
30
31function down (options) {
32 throw new Error('Not implemented.')
33}
34
35export {
36 up,
37 down
38}
diff --git a/server/initializers/migrations/0515-video-abuse-reason-timestamps.ts b/server/initializers/migrations/0515-video-abuse-reason-timestamps.ts
deleted file mode 100644
index c58335617..000000000
--- a/server/initializers/migrations/0515-video-abuse-reason-timestamps.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 await utils.queryInterface.addColumn('videoAbuse', 'predefinedReasons', {
9 type: Sequelize.ARRAY(Sequelize.INTEGER),
10 allowNull: true
11 })
12
13 await utils.queryInterface.addColumn('videoAbuse', 'startAt', {
14 type: Sequelize.INTEGER,
15 allowNull: true
16 })
17
18 await utils.queryInterface.addColumn('videoAbuse', 'endAt', {
19 type: Sequelize.INTEGER,
20 allowNull: true
21 })
22}
23
24function down (options) {
25 throw new Error('Not implemented.')
26}
27
28export {
29 up,
30 down
31}
diff --git a/server/initializers/migrations/0520-abuses-split.ts b/server/initializers/migrations/0520-abuses-split.ts
deleted file mode 100644
index 136d5c2b2..000000000
--- a/server/initializers/migrations/0520-abuses-split.ts
+++ /dev/null
@@ -1,83 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 await utils.queryInterface.renameTable('videoAbuse', 'abuse')
9
10 await utils.sequelize.query(`
11 ALTER TABLE "abuse"
12 ADD COLUMN "flaggedAccountId" INTEGER REFERENCES "account" ("id") ON DELETE SET NULL ON UPDATE CASCADE
13 `)
14
15 await utils.sequelize.query(`
16 UPDATE "abuse" SET "videoId" = NULL
17 WHERE "videoId" NOT IN (SELECT "id" FROM "video")
18 `)
19
20 await utils.sequelize.query(`
21 UPDATE "abuse" SET "flaggedAccountId" = "videoChannel"."accountId"
22 FROM "video" INNER JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id"
23 WHERE "abuse"."videoId" = "video"."id"
24 `)
25
26 await utils.sequelize.query('DROP INDEX IF EXISTS video_abuse_video_id;')
27 await utils.sequelize.query('DROP INDEX IF EXISTS video_abuse_reporter_account_id;')
28
29 await utils.sequelize.query(`
30 CREATE TABLE IF NOT EXISTS "videoAbuse" (
31 "id" serial,
32 "startAt" integer DEFAULT NULL,
33 "endAt" integer DEFAULT NULL,
34 "deletedVideo" jsonb DEFAULT NULL,
35 "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
36 "videoId" integer REFERENCES "video" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
37 "createdAt" TIMESTAMP WITH time zone NOT NULL,
38 "updatedAt" timestamp WITH time zone NOT NULL,
39 PRIMARY KEY ("id")
40 );
41 `)
42
43 await utils.sequelize.query(`
44 CREATE TABLE IF NOT EXISTS "commentAbuse" (
45 "id" serial,
46 "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
47 "videoCommentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
48 "createdAt" timestamp WITH time zone NOT NULL,
49 "updatedAt" timestamp WITH time zone NOT NULL,
50 PRIMARY KEY ("id")
51 );
52 `)
53
54 await utils.sequelize.query(`
55 INSERT INTO "videoAbuse" ("startAt", "endAt", "deletedVideo", "abuseId", "videoId", "createdAt", "updatedAt")
56 SELECT "abuse"."startAt", "abuse"."endAt", "abuse"."deletedVideo", "abuse"."id", "abuse"."videoId",
57 "abuse"."createdAt", "abuse"."updatedAt"
58 FROM "abuse"
59 `)
60
61 await utils.queryInterface.removeColumn('abuse', 'startAt')
62 await utils.queryInterface.removeColumn('abuse', 'endAt')
63 await utils.queryInterface.removeColumn('abuse', 'deletedVideo')
64 await utils.queryInterface.removeColumn('abuse', 'videoId')
65
66 await utils.sequelize.query('DROP INDEX IF EXISTS user_notification_video_abuse_id')
67 await utils.queryInterface.renameColumn('userNotification', 'videoAbuseId', 'abuseId')
68
69 await utils.sequelize.query(
70 'ALTER INDEX IF EXISTS "videoAbuse_pkey" RENAME TO "abuse_pkey"'
71 )
72
73 await utils.queryInterface.renameColumn('userNotificationSetting', 'videoAbuseAsModerator', 'abuseAsModerator')
74}
75
76function down (options) {
77 throw new Error('Not implemented.')
78}
79
80export {
81 up,
82 down
83}
diff --git a/server/initializers/migrations/0525-abuse-messages.ts b/server/initializers/migrations/0525-abuse-messages.ts
deleted file mode 100644
index c8fd7cbcf..000000000
--- a/server/initializers/migrations/0525-abuse-messages.ts
+++ /dev/null
@@ -1,54 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 await utils.sequelize.query(`
9 CREATE TABLE IF NOT EXISTS "abuseMessage" (
10 "id" serial,
11 "message" text NOT NULL,
12 "byModerator" boolean NOT NULL,
13 "accountId" integer REFERENCES "account" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
14 "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
15 "createdAt" timestamp WITH time zone NOT NULL,
16 "updatedAt" timestamp WITH time zone NOT NULL,
17 PRIMARY KEY ("id")
18 );
19 `)
20
21 const notificationSettingColumns = [ 'abuseStateChange', 'abuseNewMessage' ]
22
23 for (const column of notificationSettingColumns) {
24 const data = {
25 type: Sequelize.INTEGER,
26 defaultValue: null,
27 allowNull: true
28 }
29 await utils.queryInterface.addColumn('userNotificationSetting', column, data)
30 }
31
32 {
33 const query = 'UPDATE "userNotificationSetting" SET "abuseStateChange" = 3, "abuseNewMessage" = 3'
34 await utils.sequelize.query(query)
35 }
36
37 for (const column of notificationSettingColumns) {
38 const data = {
39 type: Sequelize.INTEGER,
40 defaultValue: null,
41 allowNull: false
42 }
43 await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
44 }
45}
46
47function down (options) {
48 throw new Error('Not implemented.')
49}
50
51export {
52 up,
53 down
54}
diff --git a/server/initializers/migrations/0530-playlist-multiple-video.ts b/server/initializers/migrations/0530-playlist-multiple-video.ts
deleted file mode 100644
index 51a8c06b0..000000000
--- a/server/initializers/migrations/0530-playlist-multiple-video.ts
+++ /dev/null
@@ -1,46 +0,0 @@
1import * as Sequelize from 'sequelize'
2import { WEBSERVER } from '../constants'
3
4async function up (utils: {
5 transaction: Sequelize.Transaction
6 queryInterface: Sequelize.QueryInterface
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
9 {
10 const field = {
11 type: Sequelize.STRING,
12 allowNull: true
13 }
14 await utils.queryInterface.changeColumn('videoPlaylistElement', 'url', field)
15 }
16
17 {
18 await utils.sequelize.query('DROP INDEX IF EXISTS video_playlist_element_video_playlist_id_video_id;')
19 }
20
21 {
22 const selectPlaylistUUID = 'SELECT "uuid" FROM "videoPlaylist" WHERE "id" = "videoPlaylistElement"."videoPlaylistId"'
23 const url = `'${WEBSERVER.URL}' || '/video-playlists/' || (${selectPlaylistUUID}) || '/videos/' || "videoPlaylistElement"."id"`
24
25 const query = `
26 UPDATE "videoPlaylistElement" SET "url" = ${url} WHERE id IN (
27 SELECT "videoPlaylistElement"."id" FROM "videoPlaylistElement"
28 INNER JOIN "videoPlaylist" ON "videoPlaylist".id = "videoPlaylistElement"."videoPlaylistId"
29 INNER JOIN account ON account.id = "videoPlaylist"."ownerAccountId"
30 INNER JOIN actor ON actor.id = account."actorId"
31 WHERE actor."serverId" IS NULL
32 )`
33
34 await utils.sequelize.query(query)
35 }
36
37}
38
39function down (options) {
40 throw new Error('Not implemented.')
41}
42
43export {
44 up,
45 down
46}
diff --git a/server/initializers/migrations/0535-video-live.ts b/server/initializers/migrations/0535-video-live.ts
deleted file mode 100644
index 7501e080b..000000000
--- a/server/initializers/migrations/0535-video-live.ts
+++ /dev/null
@@ -1,39 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 {
9 const query = `
10 CREATE TABLE IF NOT EXISTS "videoLive" (
11 "id" SERIAL ,
12 "streamKey" VARCHAR(255),
13 "videoId" INTEGER NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
14 "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
15 "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
16 PRIMARY KEY ("id")
17 );
18 `
19
20 await utils.sequelize.query(query)
21 }
22
23 {
24 await utils.queryInterface.addColumn('video', 'isLive', {
25 type: Sequelize.BOOLEAN,
26 defaultValue: false,
27 allowNull: false
28 })
29 }
30}
31
32function down (options) {
33 throw new Error('Not implemented.')
34}
35
36export {
37 up,
38 down
39}
diff --git a/server/initializers/migrations/0540-video-file-infohash.ts b/server/initializers/migrations/0540-video-file-infohash.ts
deleted file mode 100644
index 550178dab..000000000
--- a/server/initializers/migrations/0540-video-file-infohash.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 {
9 const data = {
10 type: Sequelize.STRING,
11 defaultValue: null,
12 allowNull: true
13 }
14
15 await utils.queryInterface.changeColumn('videoFile', 'infoHash', data)
16 }
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/initializers/migrations/0545-video-live-save-replay.ts b/server/initializers/migrations/0545-video-live-save-replay.ts
deleted file mode 100644
index b79a406a4..000000000
--- a/server/initializers/migrations/0545-video-live-save-replay.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 {
9 const data = {
10 type: Sequelize.BOOLEAN,
11 defaultValue: false,
12 allowNull: false
13 }
14
15 await utils.queryInterface.addColumn('videoLive', 'saveReplay', data)
16 }
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/initializers/migrations/0550-actor-follow-cleanup.ts b/server/initializers/migrations/0550-actor-follow-cleanup.ts
deleted file mode 100644
index 8ba6feec2..000000000
--- a/server/initializers/migrations/0550-actor-follow-cleanup.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 const query = `
9 WITH t AS (
10 SELECT actor.id FROM actor
11 LEFT JOIN "videoChannel" ON "videoChannel"."actorId" = actor.id
12 LEFT JOIN account ON account."actorId" = "actor"."id"
13 WHERE "videoChannel".id IS NULL and "account".id IS NULL
14 ) DELETE FROM "actorFollow" WHERE "actorId" IN (SELECT t.id FROM t) OR "targetActorId" in (SELECT t.id FROM t)
15 `
16
17 await utils.sequelize.query(query)
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0555-actor-follow-url.ts b/server/initializers/migrations/0555-actor-follow-url.ts
deleted file mode 100644
index 36431507f..000000000
--- a/server/initializers/migrations/0555-actor-follow-url.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 {
9 const data = {
10 type: Sequelize.STRING(2000),
11 defaultValue: null,
12 allowNull: true
13 }
14
15 await utils.queryInterface.addColumn('actorFollow', 'url', data)
16 }
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/initializers/migrations/0560-user-feed-token.ts b/server/initializers/migrations/0560-user-feed-token.ts
deleted file mode 100644
index 4c85b04f7..000000000
--- a/server/initializers/migrations/0560-user-feed-token.ts
+++ /dev/null
@@ -1,51 +0,0 @@
1import * as Sequelize from 'sequelize'
2import { buildUUID } from '@shared/extra-utils'
3
4async function up (utils: {
5 transaction: Sequelize.Transaction
6 queryInterface: Sequelize.QueryInterface
7 sequelize: Sequelize.Sequelize
8 db: any
9}): Promise<void> {
10 const q = utils.queryInterface
11
12 {
13 // Create uuid column for users
14 const userFeedTokenUUID = {
15 type: Sequelize.UUID,
16 defaultValue: Sequelize.UUIDV4,
17 allowNull: true
18 }
19 await q.addColumn('user', 'feedToken', userFeedTokenUUID)
20 }
21
22 // Set UUID to previous users
23 {
24 const query = 'SELECT * FROM "user" WHERE "feedToken" IS NULL'
25 const options = { type: Sequelize.QueryTypes.SELECT as Sequelize.QueryTypes.SELECT }
26 const users = await utils.sequelize.query<any>(query, options)
27
28 for (const user of users) {
29 const queryUpdate = `UPDATE "user" SET "feedToken" = '${buildUUID()}' WHERE id = ${user.id}`
30 await utils.sequelize.query(queryUpdate)
31 }
32 }
33
34 {
35 const userFeedTokenUUID = {
36 type: Sequelize.UUID,
37 defaultValue: Sequelize.UUIDV4,
38 allowNull: false
39 }
40 await q.changeColumn('user', 'feedToken', userFeedTokenUUID)
41 }
42}
43
44function down (options) {
45 throw new Error('Not implemented.')
46}
47
48export {
49 up,
50 down
51}
diff --git a/server/initializers/migrations/0565-actor-follow-local-url.ts b/server/initializers/migrations/0565-actor-follow-local-url.ts
deleted file mode 100644
index 779a59475..000000000
--- a/server/initializers/migrations/0565-actor-follow-local-url.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const query = `
11 UPDATE "actorFollow" SET url = follower.url || '/follows/' || following.id
12 FROM actor follower, actor following
13 WHERE follower."serverId" IS NULL AND follower.id = "actorFollow"."actorId" AND following.id = "actorFollow"."targetActorId"
14 `
15
16 await utils.sequelize.query(query)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0570-permanent-live.ts b/server/initializers/migrations/0570-permanent-live.ts
deleted file mode 100644
index 9572a9b2d..000000000
--- a/server/initializers/migrations/0570-permanent-live.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.BOOLEAN,
12 defaultValue: false,
13 allowNull: false
14 }
15
16 await utils.queryInterface.addColumn('videoLive', 'permanentLive', data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0575-duplicate-thumbnail.ts b/server/initializers/migrations/0575-duplicate-thumbnail.ts
deleted file mode 100644
index 4dbbe71d4..000000000
--- a/server/initializers/migrations/0575-duplicate-thumbnail.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const query = 'DELETE FROM "thumbnail" s1 ' +
11 'USING (SELECT MIN(id) as id, "filename", "type" FROM "thumbnail" GROUP BY "filename", "type" HAVING COUNT(*) > 1) s2 ' +
12 'WHERE s1."filename" = s2."filename" AND s1."type" = s2."type" AND s1.id <> s2.id'
13 await utils.sequelize.query(query)
14 }
15}
16
17function down (options) {
18 throw new Error('Not implemented.')
19}
20
21export {
22 up,
23 down
24}
diff --git a/server/initializers/migrations/0580-caption-filename.ts b/server/initializers/migrations/0580-caption-filename.ts
deleted file mode 100644
index 5281fd0c0..000000000
--- a/server/initializers/migrations/0580-caption-filename.ts
+++ /dev/null
@@ -1,48 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.STRING,
12 allowNull: true,
13 defaultValue: null
14 }
15
16 await utils.queryInterface.addColumn('videoCaption', 'filename', data)
17 }
18
19 {
20 const query = `UPDATE "videoCaption" SET "filename" = s.uuid || '-' || s.language || '.vtt' ` +
21 `FROM (` +
22 ` SELECT "videoCaption"."id", video.uuid, "videoCaption".language ` +
23 ` FROM "videoCaption" INNER JOIN video ON video.id = "videoCaption"."videoId"` +
24 `) AS s ` +
25 `WHERE "videoCaption".id = s.id`
26
27 await utils.sequelize.query(query)
28 }
29
30 {
31 const data = {
32 type: Sequelize.STRING,
33 allowNull: false,
34 defaultValue: null
35 }
36
37 await utils.queryInterface.changeColumn('videoCaption', 'filename', data)
38 }
39}
40
41function down (options) {
42 throw new Error('Not implemented.')
43}
44
45export {
46 up,
47 down
48}
diff --git a/server/initializers/migrations/0585-video-file-names.ts b/server/initializers/migrations/0585-video-file-names.ts
deleted file mode 100644
index dd5fec3a1..000000000
--- a/server/initializers/migrations/0585-video-file-names.ts
+++ /dev/null
@@ -1,55 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 for (const column of [ 'filename', 'fileUrl', 'torrentFilename', 'torrentUrl' ]) {
10 const data = {
11 type: Sequelize.STRING,
12 allowNull: true,
13 defaultValue: null
14 }
15
16 await utils.queryInterface.addColumn('videoFile', column, data)
17 }
18
19 // Generate filenames for webtorrent files
20 {
21 const webtorrentQuery = `SELECT "videoFile".id, "video".uuid, "videoFile".resolution, "videoFile".extname ` +
22 `FROM video INNER JOIN "videoFile" ON "videoFile"."videoId" = video.id`
23
24 const query = `UPDATE "videoFile" ` +
25 `SET filename = t.uuid || '-' || t.resolution || t.extname, ` +
26 `"torrentFilename" = t.uuid || '-' || t.resolution || '.torrent' ` +
27 `FROM (${webtorrentQuery}) AS t WHERE t.id = "videoFile"."id"`
28
29 await utils.sequelize.query(query)
30 }
31
32 // Generate filenames for HLS files
33 {
34 const hlsQuery = `SELECT "videoFile".id, "video".uuid, "videoFile".resolution, "videoFile".extname ` +
35 `FROM video ` +
36 `INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."videoId" = video.id ` +
37 `INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id`
38
39 const query = `UPDATE "videoFile" ` +
40 `SET filename = t.uuid || '-' || t.resolution || '-fragmented' || t.extname, ` +
41 `"torrentFilename" = t.uuid || '-' || t.resolution || '-hls.torrent' ` +
42 `FROM (${hlsQuery}) AS t WHERE t.id = "videoFile"."id"`
43
44 await utils.sequelize.query(query)
45 }
46}
47
48function down (options) {
49 throw new Error('Not implemented.')
50}
51
52export {
53 up,
54 down
55}
diff --git a/server/initializers/migrations/0590-trackers.ts b/server/initializers/migrations/0590-trackers.ts
deleted file mode 100644
index 47b9022a3..000000000
--- a/server/initializers/migrations/0590-trackers.ts
+++ /dev/null
@@ -1,44 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const query = `CREATE TABLE IF NOT EXISTS "tracker" (
11 "id" serial,
12 "url" varchar(255) NOT NULL,
13 "createdAt" timestamp WITH time zone NOT NULL,
14 "updatedAt" timestamp WITH time zone NOT NULL,
15 PRIMARY KEY ("id")
16 );`
17
18 await utils.sequelize.query(query)
19 }
20
21 {
22 const query = `CREATE TABLE IF NOT EXISTS "videoTracker" (
23 "videoId" integer REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
24 "trackerId" integer REFERENCES "tracker" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
25 "createdAt" timestamp WITH time zone NOT NULL,
26 "updatedAt" timestamp WITH time zone NOT NULL,
27 UNIQUE ("videoId", "trackerId"),
28 PRIMARY KEY ("videoId", "trackerId")
29 );`
30
31 await utils.sequelize.query(query)
32 }
33
34 await utils.sequelize.query(`CREATE UNIQUE INDEX "tracker_url" ON "tracker" ("url")`)
35}
36
37function down (options) {
38 throw new Error('Not implemented.')
39}
40
41export {
42 up,
43 down
44}
diff --git a/server/initializers/migrations/0595-remote-url.ts b/server/initializers/migrations/0595-remote-url.ts
deleted file mode 100644
index 85b367555..000000000
--- a/server/initializers/migrations/0595-remote-url.ts
+++ /dev/null
@@ -1,130 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 // Torrent and file URLs
11 {
12 const fromQueryWebtorrent = `SELECT 'https://' || server.host AS "serverUrl", '/static/webseed/' AS "filePath", "videoFile".id ` +
13 `FROM video ` +
14 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
15 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
16 `INNER JOIN server ON server.id = actor."serverId" ` +
17 `INNER JOIN "videoFile" ON "videoFile"."videoId" = video.id ` +
18 `WHERE video.remote IS TRUE`
19
20 const fromQueryHLS = `SELECT 'https://' || server.host AS "serverUrl", ` +
21 `'/static/streaming-playlists/hls/' || video.uuid || '/' AS "filePath", "videoFile".id ` +
22 `FROM video ` +
23 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
24 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
25 `INNER JOIN server ON server.id = actor."serverId" ` +
26 `INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."videoId" = video.id ` +
27 `INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ` +
28 `WHERE video.remote IS TRUE`
29
30 for (const fromQuery of [ fromQueryWebtorrent, fromQueryHLS ]) {
31 const query = `UPDATE "videoFile" ` +
32 `SET "torrentUrl" = t."serverUrl" || '/static/torrents/' || "videoFile"."torrentFilename", ` +
33 `"fileUrl" = t."serverUrl" || t."filePath" || "videoFile"."filename" ` +
34 `FROM (${fromQuery}) AS t WHERE t.id = "videoFile"."id" AND "videoFile"."fileUrl" IS NULL`
35
36 await utils.sequelize.query(query)
37 }
38 }
39
40 // Caption URLs
41 {
42 const fromQuery = `SELECT 'https://' || server.host AS "serverUrl", "video".uuid, "videoCaption".id ` +
43 `FROM video ` +
44 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
45 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
46 `INNER JOIN server ON server.id = actor."serverId" ` +
47 `INNER JOIN "videoCaption" ON "videoCaption"."videoId" = video.id ` +
48 `WHERE video.remote IS TRUE`
49
50 const query = `UPDATE "videoCaption" ` +
51 `SET "fileUrl" = t."serverUrl" || '/lazy-static/video-captions/' || t.uuid || '-' || "videoCaption"."language" || '.vtt' ` +
52 `FROM (${fromQuery}) AS t WHERE t.id = "videoCaption"."id" AND "videoCaption"."fileUrl" IS NULL`
53
54 await utils.sequelize.query(query)
55 }
56
57 // Thumbnail URLs
58 {
59 const fromQuery = `SELECT 'https://' || server.host AS "serverUrl", "video".uuid, "thumbnail".id ` +
60 `FROM video ` +
61 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
62 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
63 `INNER JOIN server ON server.id = actor."serverId" ` +
64 `INNER JOIN "thumbnail" ON "thumbnail"."videoId" = video.id ` +
65 `WHERE video.remote IS TRUE`
66
67 // Thumbnails
68 {
69 const query = `UPDATE "thumbnail" ` +
70 `SET "fileUrl" = t."serverUrl" || '/static/thumbnails/' || t.uuid || '.jpg' ` +
71 `FROM (${fromQuery}) AS t WHERE t.id = "thumbnail"."id" AND "thumbnail"."fileUrl" IS NULL AND thumbnail.type = 1`
72
73 await utils.sequelize.query(query)
74 }
75
76 {
77 // Previews
78 const query = `UPDATE "thumbnail" ` +
79 `SET "fileUrl" = t."serverUrl" || '/lazy-static/previews/' || t.uuid || '.jpg' ` +
80 `FROM (${fromQuery}) AS t WHERE t.id = "thumbnail"."id" AND "thumbnail"."fileUrl" IS NULL AND thumbnail.type = 2`
81
82 await utils.sequelize.query(query)
83 }
84 }
85
86 // Trackers
87 {
88 const trackerUrls = [
89 `'https://' || server.host || '/tracker/announce'`,
90 `'wss://' || server.host || '/tracker/socket'`
91 ]
92
93 for (const trackerUrl of trackerUrls) {
94 {
95 const query = `INSERT INTO "tracker" ("url", "createdAt", "updatedAt") ` +
96 `SELECT ${trackerUrl} AS "url", NOW(), NOW() ` +
97 `FROM video ` +
98 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
99 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
100 `INNER JOIN server ON server.id = actor."serverId" ` +
101 `WHERE video.remote IS TRUE ` +
102 `ON CONFLICT DO NOTHING`
103
104 await utils.sequelize.query(query)
105 }
106
107 {
108 const query = `INSERT INTO "videoTracker" ("videoId", "trackerId", "createdAt", "updatedAt") ` +
109 `SELECT video.id, (SELECT tracker.id FROM tracker WHERE url = ${trackerUrl}) AS "trackerId", NOW(), NOW()` +
110 `FROM video ` +
111 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
112 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
113 `INNER JOIN server ON server.id = actor."serverId" ` +
114 `WHERE video.remote IS TRUE`
115
116 await utils.sequelize.query(query)
117 }
118 }
119 }
120
121}
122
123function down (options) {
124 throw new Error('Not implemented.')
125}
126
127export {
128 up,
129 down
130}
diff --git a/server/initializers/migrations/0600-duplicate-video-files.ts b/server/initializers/migrations/0600-duplicate-video-files.ts
deleted file mode 100644
index 92d774a79..000000000
--- a/server/initializers/migrations/0600-duplicate-video-files.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 const query = 'DELETE FROM "videoFile" f1 ' +
12 'USING (SELECT MIN(id) as id, "torrentFilename" FROM "videoFile" GROUP BY "torrentFilename" HAVING COUNT(*) > 1) f2 ' +
13 'WHERE f1."torrentFilename" = f2."torrentFilename" AND f1.id <> f2.id'
14 await utils.sequelize.query(query)
15 }
16
17 {
18 const query = 'DELETE FROM "videoFile" f1 ' +
19 'USING (SELECT MIN(id) as id, "filename" FROM "videoFile" GROUP BY "filename" HAVING COUNT(*) > 1) f2 ' +
20 'WHERE f1."filename" = f2."filename" AND f1.id <> f2.id'
21 await utils.sequelize.query(query)
22 }
23
24}
25
26function down (options) {
27 throw new Error('Not implemented.')
28}
29
30export {
31 up,
32 down
33}
diff --git a/server/initializers/migrations/0605-actor-missing-keys.ts b/server/initializers/migrations/0605-actor-missing-keys.ts
deleted file mode 100644
index aa89a500c..000000000
--- a/server/initializers/migrations/0605-actor-missing-keys.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import * as Sequelize from 'sequelize'
2import { generateRSAKeyPairPromise } from '../../helpers/core-utils'
3import { PRIVATE_RSA_KEY_SIZE } from '../constants'
4
5async function up (utils: {
6 transaction: Sequelize.Transaction
7 queryInterface: Sequelize.QueryInterface
8 sequelize: Sequelize.Sequelize
9 db: any
10}): Promise<void> {
11
12 {
13 const query = 'SELECT * FROM "actor" WHERE "serverId" IS NULL AND "publicKey" IS NULL'
14 const options = { type: Sequelize.QueryTypes.SELECT as Sequelize.QueryTypes.SELECT }
15 const actors = await utils.sequelize.query<any>(query, options)
16
17 for (const actor of actors) {
18 const { privateKey, publicKey } = await generateRSAKeyPairPromise(PRIVATE_RSA_KEY_SIZE)
19
20 const queryUpdate = `UPDATE "actor" SET "publicKey" = '${publicKey}', "privateKey" = '${privateKey}' WHERE id = ${actor.id}`
21 await utils.sequelize.query(queryUpdate)
22 }
23 }
24}
25
26function down (options) {
27 throw new Error('Not implemented.')
28}
29
30export {
31 up,
32 down
33}
diff --git a/server/initializers/migrations/0610-views-index copy.ts b/server/initializers/migrations/0610-views-index copy.ts
deleted file mode 100644
index 02ee21172..000000000
--- a/server/initializers/migrations/0610-views-index copy.ts
+++ /dev/null
@@ -1,20 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 await utils.sequelize.query('DROP INDEX IF EXISTS video_views;')
11}
12
13function down (options) {
14 throw new Error('Not implemented.')
15}
16
17export {
18 up,
19 down
20}
diff --git a/server/initializers/migrations/0612-captions-unique.ts b/server/initializers/migrations/0612-captions-unique.ts
deleted file mode 100644
index 368838a2a..000000000
--- a/server/initializers/migrations/0612-captions-unique.ts
+++ /dev/null
@@ -1,23 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 await utils.sequelize.query(
11 'DELETE FROM "videoCaption" v1 USING (SELECT MIN(id) as id, "filename" FROM "videoCaption" ' +
12 'GROUP BY "filename" HAVING COUNT(*) > 1) v2 WHERE v1."filename" = v2."filename" AND v1.id <> v2.id'
13 )
14}
15
16function down (options) {
17 throw new Error('Not implemented.')
18}
19
20export {
21 up,
22 down
23}
diff --git a/server/initializers/migrations/0615-latest-versions-notification-settings.ts b/server/initializers/migrations/0615-latest-versions-notification-settings.ts
deleted file mode 100644
index 86bf56009..000000000
--- a/server/initializers/migrations/0615-latest-versions-notification-settings.ts
+++ /dev/null
@@ -1,44 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const notificationSettingColumns = [ 'newPeerTubeVersion', 'newPluginVersion' ]
11
12 for (const column of notificationSettingColumns) {
13 const data = {
14 type: Sequelize.INTEGER,
15 defaultValue: null,
16 allowNull: true
17 }
18 await utils.queryInterface.addColumn('userNotificationSetting', column, data)
19 }
20
21 {
22 const query = 'UPDATE "userNotificationSetting" SET "newPeerTubeVersion" = 3, "newPluginVersion" = 1'
23 await utils.sequelize.query(query)
24 }
25
26 for (const column of notificationSettingColumns) {
27 const data = {
28 type: Sequelize.INTEGER,
29 defaultValue: null,
30 allowNull: false
31 }
32 await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
33 }
34 }
35}
36
37function down (options) {
38 throw new Error('Not implemented.')
39}
40
41export {
42 up,
43 down
44}
diff --git a/server/initializers/migrations/0620-latest-versions-application.ts b/server/initializers/migrations/0620-latest-versions-application.ts
deleted file mode 100644
index a689b18fc..000000000
--- a/server/initializers/migrations/0620-latest-versions-application.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 const data = {
12 type: Sequelize.STRING,
13 defaultValue: null,
14 allowNull: true
15 }
16 await utils.queryInterface.addColumn('application', 'latestPeerTubeVersion', data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0625-latest-versions-notification.ts b/server/initializers/migrations/0625-latest-versions-notification.ts
deleted file mode 100644
index 77f395ce4..000000000
--- a/server/initializers/migrations/0625-latest-versions-notification.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 await utils.sequelize.query(`
12 ALTER TABLE "userNotification"
13 ADD COLUMN "applicationId" INTEGER REFERENCES "application" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
14 ADD COLUMN "pluginId" INTEGER REFERENCES "plugin" ("id") ON DELETE SET NULL ON UPDATE CASCADE
15 `)
16 }
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/initializers/migrations/0630-banner.ts b/server/initializers/migrations/0630-banner.ts
deleted file mode 100644
index 5766bb171..000000000
--- a/server/initializers/migrations/0630-banner.ts
+++ /dev/null
@@ -1,50 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 await utils.sequelize.query(`ALTER TABLE "avatar" RENAME to "actorImage"`)
12 }
13
14 {
15 const data = {
16 type: Sequelize.INTEGER,
17 defaultValue: null,
18 allowNull: true
19 }
20 await utils.queryInterface.addColumn('actorImage', 'type', data)
21 }
22
23 {
24 await utils.sequelize.query(`UPDATE "actorImage" SET "type" = 1`)
25 }
26
27 {
28 const data = {
29 type: Sequelize.INTEGER,
30 defaultValue: null,
31 allowNull: false
32 }
33 await utils.queryInterface.changeColumn('actorImage', 'type', data)
34 }
35
36 {
37 await utils.sequelize.query(
38 `ALTER TABLE "actor" ADD COLUMN "bannerId" INTEGER REFERENCES "actorImage" ("id") ON DELETE SET NULL ON UPDATE CASCADE`
39 )
40 }
41}
42
43function down (options) {
44 throw new Error('Not implemented.')
45}
46
47export {
48 up,
49 down
50}
diff --git a/server/initializers/migrations/0635-actor-image-size.ts b/server/initializers/migrations/0635-actor-image-size.ts
deleted file mode 100644
index d7c5da8c3..000000000
--- a/server/initializers/migrations/0635-actor-image-size.ts
+++ /dev/null
@@ -1,35 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.INTEGER,
12 defaultValue: null,
13 allowNull: true
14 }
15 await utils.queryInterface.addColumn('actorImage', 'height', data)
16 }
17
18 {
19 const data = {
20 type: Sequelize.INTEGER,
21 defaultValue: null,
22 allowNull: true
23 }
24 await utils.queryInterface.addColumn('actorImage', 'width', data)
25 }
26}
27
28function down (options) {
29 throw new Error('Not implemented.')
30}
31
32export {
33 up,
34 down
35}
diff --git a/server/initializers/migrations/0640-unique-keys.ts b/server/initializers/migrations/0640-unique-keys.ts
deleted file mode 100644
index b082accc2..000000000
--- a/server/initializers/migrations/0640-unique-keys.ts
+++ /dev/null
@@ -1,39 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 await utils.sequelize.query(
11 'DELETE FROM "actor" v1 USING (SELECT MIN(id) as id, "preferredUsername", "serverId" FROM "actor" ' +
12 'GROUP BY "preferredUsername", "serverId" HAVING COUNT(*) > 1 AND "serverId" IS NOT NULL) v2 ' +
13 'WHERE v1."preferredUsername" = v2."preferredUsername" AND v1."serverId" = v2."serverId" AND v1.id <> v2.id'
14 )
15 }
16
17 {
18 await utils.sequelize.query(
19 'DELETE FROM "actor" v1 USING (SELECT MIN(id) as id, "url" FROM "actor" GROUP BY "url" HAVING COUNT(*) > 1) v2 ' +
20 'WHERE v1."url" = v2."url" AND v1.id <> v2.id'
21 )
22 }
23
24 {
25 await utils.sequelize.query(
26 'DELETE FROM "tag" v1 USING (SELECT MIN(id) as id, "name" FROM "tag" GROUP BY "name" HAVING COUNT(*) > 1) v2 ' +
27 'WHERE v1."name" = v2."name" AND v1.id <> v2.id'
28 )
29 }
30}
31
32function down (options) {
33 throw new Error('Not implemented.')
34}
35
36export {
37 up,
38 down
39}
diff --git a/server/initializers/migrations/0645-actor-remote-creation-date.ts b/server/initializers/migrations/0645-actor-remote-creation-date.ts
deleted file mode 100644
index 38b3b881c..000000000
--- a/server/initializers/migrations/0645-actor-remote-creation-date.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.DATE,
12 defaultValue: null,
13 allowNull: true
14 }
15 await utils.queryInterface.addColumn('actor', 'remoteCreatedAt', data)
16 }
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/initializers/migrations/0650-actor-custom-pages.ts b/server/initializers/migrations/0650-actor-custom-pages.ts
deleted file mode 100644
index 1338327e8..000000000
--- a/server/initializers/migrations/0650-actor-custom-pages.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const query = `
11 CREATE TABLE IF NOT EXISTS "actorCustomPage" (
12 "id" serial,
13 "content" TEXT,
14 "type" varchar(255) NOT NULL,
15 "actorId" integer NOT NULL REFERENCES "actor" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
16 "createdAt" timestamp WITH time zone NOT NULL,
17 "updatedAt" timestamp WITH time zone NOT NULL,
18 PRIMARY KEY ("id")
19 );
20 `
21
22 await utils.sequelize.query(query)
23 }
24}
25
26function down (options) {
27 throw new Error('Not implemented.')
28}
29
30export {
31 up,
32 down
33}
diff --git a/server/initializers/migrations/0655-streaming-playlist-filenames.ts b/server/initializers/migrations/0655-streaming-playlist-filenames.ts
deleted file mode 100644
index 9172a22c4..000000000
--- a/server/initializers/migrations/0655-streaming-playlist-filenames.ts
+++ /dev/null
@@ -1,66 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 for (const column of [ 'playlistUrl', 'segmentsSha256Url' ]) {
11 const data = {
12 type: Sequelize.STRING,
13 allowNull: true,
14 defaultValue: null
15 }
16
17 await utils.queryInterface.changeColumn('videoStreamingPlaylist', column, data)
18 }
19 }
20
21 {
22 await utils.sequelize.query(
23 `UPDATE "videoStreamingPlaylist" SET "playlistUrl" = NULL, "segmentsSha256Url" = NULL ` +
24 `WHERE "videoId" IN (SELECT id FROM video WHERE remote IS FALSE)`
25 )
26 }
27
28 {
29 for (const column of [ 'playlistFilename', 'segmentsSha256Filename' ]) {
30 const data = {
31 type: Sequelize.STRING,
32 allowNull: true,
33 defaultValue: null
34 }
35
36 await utils.queryInterface.addColumn('videoStreamingPlaylist', column, data)
37 }
38 }
39
40 {
41 await utils.sequelize.query(
42 `UPDATE "videoStreamingPlaylist" SET "playlistFilename" = 'master.m3u8', "segmentsSha256Filename" = 'segments-sha256.json'`
43 )
44 }
45
46 {
47 for (const column of [ 'playlistFilename', 'segmentsSha256Filename' ]) {
48 const data = {
49 type: Sequelize.STRING,
50 allowNull: false,
51 defaultValue: null
52 }
53
54 await utils.queryInterface.changeColumn('videoStreamingPlaylist', column, data)
55 }
56 }
57}
58
59function down (options) {
60 throw new Error('Not implemented.')
61}
62
63export {
64 up,
65 down
66}
diff --git a/server/initializers/migrations/0660-object-storage.ts b/server/initializers/migrations/0660-object-storage.ts
deleted file mode 100644
index 53cb89ce6..000000000
--- a/server/initializers/migrations/0660-object-storage.ts
+++ /dev/null
@@ -1,56 +0,0 @@
1import * as Sequelize from 'sequelize'
2import { VideoStorage } from '@shared/models'
3
4async function up (utils: {
5 transaction: Sequelize.Transaction
6 queryInterface: Sequelize.QueryInterface
7 sequelize: Sequelize.Sequelize
8 db: any
9}): Promise<void> {
10 {
11 const query = `
12 CREATE TABLE IF NOT EXISTS "videoJobInfo" (
13 "id" serial,
14 "pendingMove" INTEGER NOT NULL,
15 "pendingTranscode" INTEGER NOT NULL,
16 "videoId" serial UNIQUE NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
17 "createdAt" timestamp WITH time zone NOT NULL,
18 "updatedAt" timestamp WITH time zone NOT NULL,
19 PRIMARY KEY ("id")
20 );
21 `
22
23 await utils.sequelize.query(query)
24 }
25
26 {
27 await utils.queryInterface.addColumn('videoFile', 'storage', {
28 type: Sequelize.INTEGER,
29 allowNull: true,
30 defaultValue: VideoStorage.FILE_SYSTEM
31 })
32 await utils.queryInterface.changeColumn('videoFile', 'storage', { type: Sequelize.INTEGER, allowNull: false, defaultValue: null })
33 }
34
35 {
36 await utils.queryInterface.addColumn('videoStreamingPlaylist', 'storage', {
37 type: Sequelize.INTEGER,
38 allowNull: true,
39 defaultValue: VideoStorage.FILE_SYSTEM
40 })
41 await utils.queryInterface.changeColumn('videoStreamingPlaylist', 'storage', {
42 type: Sequelize.INTEGER,
43 allowNull: false,
44 defaultValue: null
45 })
46 }
47}
48
49function down (options) {
50 throw new Error('Not implemented.')
51}
52
53export {
54 up,
55 down
56}
diff --git a/server/initializers/migrations/0665-no-account-warning-modal.ts b/server/initializers/migrations/0665-no-account-warning-modal.ts
deleted file mode 100644
index 6718aed85..000000000
--- a/server/initializers/migrations/0665-no-account-warning-modal.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.BOOLEAN,
12 allowNull: false,
13 defaultValue: false
14 }
15
16 await utils.queryInterface.addColumn('user', 'noAccountSetupWarningModal', data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0670-pending-job-default.ts b/server/initializers/migrations/0670-pending-job-default.ts
deleted file mode 100644
index 7e9def1a0..000000000
--- a/server/initializers/migrations/0670-pending-job-default.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 for (const column of [ 'pendingMove', 'pendingTranscode' ]) {
10 const data = {
11 type: Sequelize.INTEGER,
12 allowNull: false,
13 defaultValue: 0
14 }
15
16 await utils.queryInterface.changeColumn('videoJobInfo', column, data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0675-p2p-enabled.ts b/server/initializers/migrations/0675-p2p-enabled.ts
deleted file mode 100644
index b4f53381e..000000000
--- a/server/initializers/migrations/0675-p2p-enabled.ts
+++ /dev/null
@@ -1,21 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 await utils.queryInterface.renameColumn('user', 'webTorrentEnabled', 'p2pEnabled')
10
11 await utils.sequelize.query('ALTER TABLE "user" ALTER COLUMN "p2pEnabled" DROP DEFAULT')
12}
13
14function down (options) {
15 throw new Error('Not implemented.')
16}
17
18export {
19 up,
20 down
21}
diff --git a/server/initializers/migrations/0680-files-storage-default.ts b/server/initializers/migrations/0680-files-storage-default.ts
deleted file mode 100644
index 087bc8790..000000000
--- a/server/initializers/migrations/0680-files-storage-default.ts
+++ /dev/null
@@ -1,20 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 await utils.sequelize.query('ALTER TABLE "videoFile" ALTER COLUMN "storage" SET DEFAULT 0')
10 await utils.sequelize.query('ALTER TABLE "videoStreamingPlaylist" ALTER COLUMN "storage" SET DEFAULT 0')
11}
12
13function down (options) {
14 throw new Error('Not implemented.')
15}
16
17export {
18 up,
19 down
20}
diff --git a/server/initializers/migrations/0685-multiple-actor-images.ts b/server/initializers/migrations/0685-multiple-actor-images.ts
deleted file mode 100644
index c656f7e28..000000000
--- a/server/initializers/migrations/0685-multiple-actor-images.ts
+++ /dev/null
@@ -1,62 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 await utils.queryInterface.addColumn('actorImage', 'actorId', {
11 type: Sequelize.INTEGER,
12 defaultValue: null,
13 allowNull: true,
14 references: {
15 model: 'actor',
16 key: 'id'
17 },
18 onDelete: 'CASCADE'
19 }, { transaction: utils.transaction })
20
21 // Avatars
22 {
23 const query = `UPDATE "actorImage" SET "actorId" = (SELECT "id" FROM "actor" WHERE "actor"."avatarId" = "actorImage"."id") ` +
24 `WHERE "type" = 1`
25 await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction })
26 }
27
28 // Banners
29 {
30 const query = `UPDATE "actorImage" SET "actorId" = (SELECT "id" FROM "actor" WHERE "actor"."bannerId" = "actorImage"."id") ` +
31 `WHERE "type" = 2`
32 await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction })
33 }
34
35 // Remove orphans
36 {
37 const query = `DELETE FROM "actorImage" WHERE id NOT IN (` +
38 `SELECT "bannerId" FROM actor WHERE "bannerId" IS NOT NULL ` +
39 `UNION select "avatarId" FROM actor WHERE "avatarId" IS NOT NULL` +
40 `);`
41
42 await utils.sequelize.query(query, { type: Sequelize.QueryTypes.DELETE, transaction: utils.transaction })
43 }
44
45 await utils.queryInterface.changeColumn('actorImage', 'actorId', {
46 type: Sequelize.INTEGER,
47 allowNull: false
48 }, { transaction: utils.transaction })
49
50 await utils.queryInterface.removeColumn('actor', 'avatarId', { transaction: utils.transaction })
51 await utils.queryInterface.removeColumn('actor', 'bannerId', { transaction: utils.transaction })
52 }
53}
54
55function down () {
56 throw new Error('Not implemented.')
57}
58
59export {
60 up,
61 down
62}
diff --git a/server/initializers/migrations/0690-live-latency-mode.ts b/server/initializers/migrations/0690-live-latency-mode.ts
deleted file mode 100644
index c31a61364..000000000
--- a/server/initializers/migrations/0690-live-latency-mode.ts
+++ /dev/null
@@ -1,35 +0,0 @@
1import { LiveVideoLatencyMode } from '@shared/models'
2import * as Sequelize from 'sequelize'
3
4async function up (utils: {
5 transaction: Sequelize.Transaction
6 queryInterface: Sequelize.QueryInterface
7 sequelize: Sequelize.Sequelize
8 db: any
9}): Promise<void> {
10 await utils.queryInterface.addColumn('videoLive', 'latencyMode', {
11 type: Sequelize.INTEGER,
12 defaultValue: null,
13 allowNull: true
14 }, { transaction: utils.transaction })
15
16 {
17 const query = `UPDATE "videoLive" SET "latencyMode" = ${LiveVideoLatencyMode.DEFAULT}`
18 await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction })
19 }
20
21 await utils.queryInterface.changeColumn('videoLive', 'latencyMode', {
22 type: Sequelize.INTEGER,
23 defaultValue: null,
24 allowNull: false
25 }, { transaction: utils.transaction })
26}
27
28function down () {
29 throw new Error('Not implemented.')
30}
31
32export {
33 up,
34 down
35}
diff --git a/server/initializers/migrations/0695-remove-remote-rates.ts b/server/initializers/migrations/0695-remove-remote-rates.ts
deleted file mode 100644
index f5c394bae..000000000
--- a/server/initializers/migrations/0695-remove-remote-rates.ts
+++ /dev/null
@@ -1,28 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const query = 'DELETE FROM "accountVideoRate" ' +
10 'WHERE "accountVideoRate".id IN (' +
11 'SELECT "accountVideoRate".id FROM "accountVideoRate" ' +
12 'INNER JOIN account ON account.id = "accountVideoRate"."accountId" ' +
13 'INNER JOIN actor ON actor.id = account."actorId" ' +
14 'INNER JOIN video ON video.id = "accountVideoRate"."videoId" ' +
15 'WHERE actor."serverId" IS NOT NULL AND video.remote IS TRUE' +
16 ')'
17
18 await utils.sequelize.query(query, { type: Sequelize.QueryTypes.BULKDELETE, transaction: utils.transaction })
19}
20
21function down () {
22 throw new Error('Not implemented.')
23}
24
25export {
26 up,
27 down
28}
diff --git a/server/initializers/migrations/0700-edition-finished-notification.ts b/server/initializers/migrations/0700-edition-finished-notification.ts
deleted file mode 100644
index 5310eab3f..000000000
--- a/server/initializers/migrations/0700-edition-finished-notification.ts
+++ /dev/null
@@ -1,42 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const { transaction } = utils
10
11 {
12 const data = {
13 type: Sequelize.INTEGER,
14 defaultValue: null,
15 allowNull: true
16 }
17 await utils.queryInterface.addColumn('userNotificationSetting', 'myVideoStudioEditionFinished', data, { transaction })
18 }
19
20 {
21 const query = 'UPDATE "userNotificationSetting" SET "myVideoStudioEditionFinished" = 1'
22 await utils.sequelize.query(query, { transaction })
23 }
24
25 {
26 const data = {
27 type: Sequelize.INTEGER,
28 defaultValue: null,
29 allowNull: false
30 }
31 await utils.queryInterface.changeColumn('userNotificationSetting', 'myVideoStudioEditionFinished', data, { transaction })
32 }
33}
34
35function down () {
36 throw new Error('Not implemented.')
37}
38
39export {
40 up,
41 down
42}
diff --git a/server/initializers/migrations/0705-local-video-viewers.ts b/server/initializers/migrations/0705-local-video-viewers.ts
deleted file mode 100644
index 123402641..000000000
--- a/server/initializers/migrations/0705-local-video-viewers.ts
+++ /dev/null
@@ -1,52 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const { transaction } = utils
10
11 {
12 const query = `
13 CREATE TABLE IF NOT EXISTS "localVideoViewer" (
14 "id" serial,
15 "startDate" timestamp with time zone NOT NULL,
16 "endDate" timestamp with time zone NOT NULL,
17 "watchTime" integer NOT NULL,
18 "country" varchar(255),
19 "uuid" uuid NOT NULL,
20 "url" varchar(255) NOT NULL,
21 "videoId" integer NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
22 "createdAt" timestamp with time zone NOT NULL,
23 PRIMARY KEY ("id")
24 );
25 `
26 await utils.sequelize.query(query, { transaction })
27 }
28
29 {
30 const query = `
31 CREATE TABLE IF NOT EXISTS "localVideoViewerWatchSection" (
32 "id" serial,
33 "watchStart" integer NOT NULL,
34 "watchEnd" integer NOT NULL,
35 "localVideoViewerId" integer NOT NULL REFERENCES "localVideoViewer" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
36 "createdAt" timestamp with time zone NOT NULL,
37 PRIMARY KEY ("id")
38 );
39 `
40 await utils.sequelize.query(query, { transaction })
41 }
42
43}
44
45function down () {
46 throw new Error('Not implemented.')
47}
48
49export {
50 up,
51 down
52}
diff --git a/server/initializers/migrations/0710-live-sessions.ts b/server/initializers/migrations/0710-live-sessions.ts
deleted file mode 100644
index aaac8d9ce..000000000
--- a/server/initializers/migrations/0710-live-sessions.ts
+++ /dev/null
@@ -1,34 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const { transaction } = utils
10
11 const query = `
12 CREATE TABLE IF NOT EXISTS "videoLiveSession" (
13 "id" serial,
14 "startDate" timestamp with time zone NOT NULL,
15 "endDate" timestamp with time zone,
16 "error" integer,
17 "replayVideoId" integer REFERENCES "video" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
18 "liveVideoId" integer REFERENCES "video" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
19 "createdAt" timestamp with time zone NOT NULL,
20 "updatedAt" timestamp with time zone NOT NULL,
21 PRIMARY KEY ("id")
22 );
23 `
24 await utils.sequelize.query(query, { transaction })
25}
26
27function down () {
28 throw new Error('Not implemented.')
29}
30
31export {
32 up,
33 down
34}
diff --git a/server/initializers/migrations/0715-video-source.ts b/server/initializers/migrations/0715-video-source.ts
deleted file mode 100644
index efcf77ebd..000000000
--- a/server/initializers/migrations/0715-video-source.ts
+++ /dev/null
@@ -1,34 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const query = `
11 CREATE TABLE IF NOT EXISTS "videoSource" (
12 "id" SERIAL ,
13 "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
14 "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
15 "filename" VARCHAR(255) DEFAULT NULL,
16 "videoId" INTEGER
17 REFERENCES "video" ("id")
18 ON DELETE CASCADE
19 ON UPDATE CASCADE,
20 PRIMARY KEY ("id")
21 );
22 `
23 await utils.sequelize.query(query)
24 }
25}
26
27function down (options) {
28 throw new Error('Not implemented.')
29}
30
31export {
32 up,
33 down
34}
diff --git a/server/initializers/migrations/0720-session-ending-processed.ts b/server/initializers/migrations/0720-session-ending-processed.ts
deleted file mode 100644
index 74ffb39a0..000000000
--- a/server/initializers/migrations/0720-session-ending-processed.ts
+++ /dev/null
@@ -1,56 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const { transaction } = utils
10
11 {
12 const data = {
13 type: Sequelize.BOOLEAN,
14 defaultValue: null,
15 allowNull: true
16 }
17 await utils.queryInterface.addColumn('videoLiveSession', 'endingProcessed', data, { transaction })
18 await utils.queryInterface.addColumn('videoLiveSession', 'saveReplay', data, { transaction })
19 }
20
21 {
22 const query = `UPDATE "videoLiveSession" SET "saveReplay" = (
23 SELECT "videoLive"."saveReplay" FROM "videoLive" WHERE "videoLive"."videoId" = "videoLiveSession"."liveVideoId"
24 ) WHERE "videoLiveSession"."liveVideoId" IS NOT NULL`
25 await utils.sequelize.query(query, { transaction })
26 }
27
28 {
29 const query = `UPDATE "videoLiveSession" SET "saveReplay" = FALSE WHERE "saveReplay" IS NULL`
30 await utils.sequelize.query(query, { transaction })
31 }
32
33 {
34 const query = `UPDATE "videoLiveSession" SET "endingProcessed" = TRUE`
35 await utils.sequelize.query(query, { transaction })
36 }
37
38 {
39 const data = {
40 type: Sequelize.BOOLEAN,
41 defaultValue: null,
42 allowNull: false
43 }
44 await utils.queryInterface.changeColumn('videoLiveSession', 'endingProcessed', data, { transaction })
45 await utils.queryInterface.changeColumn('videoLiveSession', 'saveReplay', data, { transaction })
46 }
47}
48
49function down (options) {
50 throw new Error('Not implemented.')
51}
52
53export {
54 up,
55 down
56}
diff --git a/server/initializers/migrations/0725-node-version.ts b/server/initializers/migrations/0725-node-version.ts
deleted file mode 100644
index d8b9cc750..000000000
--- a/server/initializers/migrations/0725-node-version.ts
+++ /dev/null
@@ -1,66 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const { transaction } = utils
10
11 {
12 const data = {
13 type: Sequelize.STRING,
14 defaultValue: null,
15 allowNull: true
16 }
17 await utils.queryInterface.addColumn('application', 'nodeVersion', data, { transaction })
18 }
19
20 {
21 const data = {
22 type: Sequelize.STRING,
23 defaultValue: null,
24 allowNull: true
25 }
26 await utils.queryInterface.addColumn('application', 'nodeABIVersion', data, { transaction })
27 }
28
29 {
30 const query = `UPDATE "application" SET "nodeVersion" = '${process.version}'`
31 await utils.sequelize.query(query, { transaction })
32 }
33
34 {
35 const nodeABIVersion = parseInt(process.versions.modules)
36 const query = `UPDATE "application" SET "nodeABIVersion" = ${nodeABIVersion}`
37 await utils.sequelize.query(query, { transaction })
38 }
39
40 {
41 const data = {
42 type: Sequelize.STRING,
43 defaultValue: null,
44 allowNull: false
45 }
46 await utils.queryInterface.changeColumn('application', 'nodeVersion', data, { transaction })
47 }
48
49 {
50 const data = {
51 type: Sequelize.STRING,
52 defaultValue: null,
53 allowNull: false
54 }
55 await utils.queryInterface.changeColumn('application', 'nodeABIVersion', data, { transaction })
56 }
57}
58
59function down (options) {
60 throw new Error('Not implemented.')
61}
62
63export {
64 up,
65 down
66}
diff --git a/server/initializers/migrations/0730-video-channel-sync.ts b/server/initializers/migrations/0730-video-channel-sync.ts
deleted file mode 100644
index a2fe8211f..000000000
--- a/server/initializers/migrations/0730-video-channel-sync.ts
+++ /dev/null
@@ -1,36 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const query = `
10 CREATE TABLE IF NOT EXISTS "videoChannelSync" (
11 "id" SERIAL,
12 "externalChannelUrl" VARCHAR(2000) NOT NULL DEFAULT NULL,
13 "videoChannelId" INTEGER NOT NULL REFERENCES "videoChannel" ("id")
14 ON DELETE CASCADE
15 ON UPDATE CASCADE,
16 "state" INTEGER NOT NULL DEFAULT 1,
17 "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
18 "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
19 "lastSyncAt" TIMESTAMP WITH TIME ZONE,
20 PRIMARY KEY ("id")
21 );
22 `
23 await utils.sequelize.query(query, { transaction: utils.transaction })
24}
25
26async function down (utils: {
27 queryInterface: Sequelize.QueryInterface
28 transaction: Sequelize.Transaction
29}) {
30 await utils.queryInterface.dropTable('videoChannelSync', { transaction: utils.transaction })
31}
32
33export {
34 up,
35 down
36}
diff --git a/server/initializers/migrations/0735-video-channel-sync-import-foreign-key.ts b/server/initializers/migrations/0735-video-channel-sync-import-foreign-key.ts
deleted file mode 100644
index ffe0b11ab..000000000
--- a/server/initializers/migrations/0735-video-channel-sync-import-foreign-key.ts
+++ /dev/null
@@ -1,32 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 await utils.queryInterface.addColumn('videoImport', 'videoChannelSyncId', {
10 type: Sequelize.INTEGER,
11 defaultValue: null,
12 allowNull: true,
13 references: {
14 model: 'videoChannelSync',
15 key: 'id'
16 },
17 onUpdate: 'CASCADE',
18 onDelete: 'SET NULL'
19 }, { transaction: utils.transaction })
20}
21
22async function down (utils: {
23 queryInterface: Sequelize.QueryInterface
24 transaction: Sequelize.Transaction
25}) {
26 await utils.queryInterface.dropTable('videoChannelSync', { transaction: utils.transaction })
27}
28
29export {
30 up,
31 down
32}
diff --git a/server/initializers/migrations/0740-fix-old-enums.ts b/server/initializers/migrations/0740-fix-old-enums.ts
deleted file mode 100644
index 501d0ccb2..000000000
--- a/server/initializers/migrations/0740-fix-old-enums.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 try {
10 await utils.sequelize.query('drop type "enum_actorFollow_state"')
11 await utils.sequelize.query('alter type "enum_AccountFollows_state" rename to "enum_actorFollow_state";')
12 } catch {
13 // empty
14 }
15
16 try {
17 await utils.sequelize.query('drop type "enum_accountVideoRate_type"')
18 await utils.sequelize.query('alter type "enum_AccountVideoRates_type" rename to "enum_accountVideoRate_type";')
19 } catch {
20 // empty
21 }
22}
23
24async function down (utils: {
25 queryInterface: Sequelize.QueryInterface
26 transaction: Sequelize.Transaction
27}) {
28}
29
30export {
31 up,
32 down
33}
diff --git a/server/initializers/migrations/0745-user-otp.ts b/server/initializers/migrations/0745-user-otp.ts
deleted file mode 100644
index 157308ea1..000000000
--- a/server/initializers/migrations/0745-user-otp.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const { transaction } = utils
10
11 const data = {
12 type: Sequelize.STRING,
13 defaultValue: null,
14 allowNull: true
15 }
16 await utils.queryInterface.addColumn('user', 'otpSecret', data, { transaction })
17
18}
19
20async function down (utils: {
21 queryInterface: Sequelize.QueryInterface
22 transaction: Sequelize.Transaction
23}) {
24}
25
26export {
27 up,
28 down
29}
diff --git a/server/initializers/migrations/0750-user-registration.ts b/server/initializers/migrations/0750-user-registration.ts
deleted file mode 100644
index 15bbfd3fd..000000000
--- a/server/initializers/migrations/0750-user-registration.ts
+++ /dev/null
@@ -1,58 +0,0 @@
1
2import * as Sequelize from 'sequelize'
3
4async function up (utils: {
5 transaction: Sequelize.Transaction
6 queryInterface: Sequelize.QueryInterface
7 sequelize: Sequelize.Sequelize
8 db: any
9}): Promise<void> {
10 {
11 const query = `
12 CREATE TABLE IF NOT EXISTS "userRegistration" (
13 "id" serial,
14 "state" integer NOT NULL,
15 "registrationReason" text NOT NULL,
16 "moderationResponse" text,
17 "password" varchar(255),
18 "username" varchar(255) NOT NULL,
19 "email" varchar(400) NOT NULL,
20 "emailVerified" boolean,
21 "accountDisplayName" varchar(255),
22 "channelHandle" varchar(255),
23 "channelDisplayName" varchar(255),
24 "userId" integer REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
25 "createdAt" timestamp with time zone NOT NULL,
26 "updatedAt" timestamp with time zone NOT NULL,
27 PRIMARY KEY ("id")
28 );
29 `
30 await utils.sequelize.query(query, { transaction: utils.transaction })
31 }
32
33 {
34 await utils.queryInterface.addColumn('userNotification', 'userRegistrationId', {
35 type: Sequelize.INTEGER,
36 defaultValue: null,
37 allowNull: true,
38 references: {
39 model: 'userRegistration',
40 key: 'id'
41 },
42 onUpdate: 'CASCADE',
43 onDelete: 'SET NULL'
44 }, { transaction: utils.transaction })
45 }
46}
47
48async function down (utils: {
49 queryInterface: Sequelize.QueryInterface
50 transaction: Sequelize.Transaction
51}) {
52 await utils.queryInterface.dropTable('videoChannelSync', { transaction: utils.transaction })
53}
54
55export {
56 up,
57 down
58}
diff --git a/server/initializers/migrations/0755-unique-viewer-url.ts b/server/initializers/migrations/0755-unique-viewer-url.ts
deleted file mode 100644
index b3dff9258..000000000
--- a/server/initializers/migrations/0755-unique-viewer-url.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const { transaction } = utils
10
11 const query = 'DELETE FROM "localVideoViewer" t1 ' +
12 'USING (SELECT MIN(id) as id, "url" FROM "localVideoViewer" GROUP BY "url" HAVING COUNT(*) > 1) t2 ' +
13 'WHERE t1."url" = t2."url" AND t1.id <> t2.id'
14
15 await utils.sequelize.query(query, { transaction })
16}
17
18async function down (utils: {
19 queryInterface: Sequelize.QueryInterface
20 transaction: Sequelize.Transaction
21}) {
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0760-video-live-replay-setting.ts b/server/initializers/migrations/0760-video-live-replay-setting.ts
deleted file mode 100644
index 7878df3f7..000000000
--- a/server/initializers/migrations/0760-video-live-replay-setting.ts
+++ /dev/null
@@ -1,125 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 {
9 const query = `
10 CREATE TABLE IF NOT EXISTS "videoLiveReplaySetting" (
11 "id" SERIAL ,
12 "privacy" INTEGER NOT NULL,
13 "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
14 "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
15 PRIMARY KEY ("id")
16 );
17 `
18
19 await utils.sequelize.query(query, { transaction : utils.transaction })
20 }
21
22 {
23 await utils.queryInterface.addColumn('videoLive', 'replaySettingId', {
24 type: Sequelize.INTEGER,
25 defaultValue: null,
26 allowNull: true,
27 references: {
28 model: 'videoLiveReplaySetting',
29 key: 'id'
30 },
31 onDelete: 'SET NULL'
32 }, { transaction: utils.transaction })
33 }
34
35 {
36 await utils.queryInterface.addColumn('videoLiveSession', 'replaySettingId', {
37 type: Sequelize.INTEGER,
38 defaultValue: null,
39 allowNull: true,
40 references: {
41 model: 'videoLiveReplaySetting',
42 key: 'id'
43 },
44 onDelete: 'SET NULL'
45 }, { transaction: utils.transaction })
46 }
47
48 {
49 const query = `
50 SELECT live."id", v."privacy"
51 FROM "videoLive" live
52 INNER JOIN "video" v ON live."videoId" = v."id"
53 WHERE live."saveReplay" = true
54 `
55
56 const videoLives = await utils.sequelize.query<{ id: number, privacy: number }>(
57 query,
58 { type: Sequelize.QueryTypes.SELECT, transaction: utils.transaction }
59 )
60
61 for (const videoLive of videoLives) {
62 const query = `
63 WITH new_replay_setting AS (
64 INSERT INTO "videoLiveReplaySetting" ("privacy", "createdAt", "updatedAt")
65 VALUES (:privacy, NOW(), NOW())
66 RETURNING id
67 )
68 UPDATE "videoLive" SET "replaySettingId" = (SELECT id FROM new_replay_setting)
69 WHERE "id" = :id
70 `
71
72 const options = {
73 replacements: { privacy: videoLive.privacy, id: videoLive.id },
74 type: Sequelize.QueryTypes.UPDATE,
75 transaction: utils.transaction
76 }
77
78 await utils.sequelize.query(query, options)
79 }
80 }
81
82 {
83 const query = `
84 SELECT session."id", v."privacy"
85 FROM "videoLiveSession" session
86 INNER JOIN "video" v ON session."liveVideoId" = v."id"
87 WHERE session."saveReplay" = true
88 AND session."liveVideoId" IS NOT NULL;
89 `
90
91 const videoLiveSessions = await utils.sequelize.query<{ id: number, privacy: number }>(
92 query,
93 { type: Sequelize.QueryTypes.SELECT, transaction: utils.transaction }
94 )
95
96 for (const videoLive of videoLiveSessions) {
97 const query = `
98 WITH new_replay_setting AS (
99 INSERT INTO "videoLiveReplaySetting" ("privacy", "createdAt", "updatedAt")
100 VALUES (:privacy, NOW(), NOW())
101 RETURNING id
102 )
103 UPDATE "videoLiveSession" SET "replaySettingId" = (SELECT id FROM new_replay_setting)
104 WHERE "id" = :id
105 `
106
107 const options = {
108 replacements: { privacy: videoLive.privacy, id: videoLive.id },
109 type: Sequelize.QueryTypes.UPDATE,
110 transaction: utils.transaction
111 }
112
113 await utils.sequelize.query(query, options)
114 }
115 }
116}
117
118function down (options) {
119 throw new Error('Not implemented.')
120}
121
122export {
123 up,
124 down
125}
diff --git a/server/initializers/migrations/0765-remote-transcoding.ts b/server/initializers/migrations/0765-remote-transcoding.ts
deleted file mode 100644
index 40cca03b4..000000000
--- a/server/initializers/migrations/0765-remote-transcoding.ts
+++ /dev/null
@@ -1,78 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 {
9 const query = `
10 CREATE TABLE IF NOT EXISTS "runnerRegistrationToken"(
11 "id" serial,
12 "registrationToken" varchar(255) NOT NULL,
13 "createdAt" timestamp with time zone NOT NULL,
14 "updatedAt" timestamp with time zone NOT NULL,
15 PRIMARY KEY ("id")
16 );
17 `
18
19 await utils.sequelize.query(query, { transaction : utils.transaction })
20 }
21
22 {
23 const query = `
24 CREATE TABLE IF NOT EXISTS "runner"(
25 "id" serial,
26 "runnerToken" varchar(255) NOT NULL,
27 "name" varchar(255) NOT NULL,
28 "description" varchar(1000),
29 "lastContact" timestamp with time zone NOT NULL,
30 "ip" varchar(255) NOT NULL,
31 "runnerRegistrationTokenId" integer REFERENCES "runnerRegistrationToken"("id") ON DELETE CASCADE ON UPDATE CASCADE,
32 "createdAt" timestamp with time zone NOT NULL,
33 "updatedAt" timestamp with time zone NOT NULL,
34 PRIMARY KEY ("id")
35 );
36 `
37
38 await utils.sequelize.query(query, { transaction : utils.transaction })
39 }
40
41 {
42 const query = `
43 CREATE TABLE IF NOT EXISTS "runnerJob"(
44 "id" serial,
45 "uuid" uuid NOT NULL,
46 "type" varchar(255) NOT NULL,
47 "payload" jsonb NOT NULL,
48 "privatePayload" jsonb NOT NULL,
49 "state" integer NOT NULL,
50 "failures" integer NOT NULL DEFAULT 0,
51 "error" varchar(5000),
52 "priority" integer NOT NULL,
53 "processingJobToken" varchar(255),
54 "progress" integer,
55 "startedAt" timestamp with time zone,
56 "finishedAt" timestamp with time zone,
57 "dependsOnRunnerJobId" integer REFERENCES "runnerJob"("id") ON DELETE CASCADE ON UPDATE CASCADE,
58 "runnerId" integer REFERENCES "runner"("id") ON DELETE SET NULL ON UPDATE CASCADE,
59 "createdAt" timestamp with time zone NOT NULL,
60 "updatedAt" timestamp with time zone NOT NULL,
61 PRIMARY KEY ("id")
62 );
63
64
65 `
66
67 await utils.sequelize.query(query, { transaction : utils.transaction })
68 }
69}
70
71function down (options) {
72 throw new Error('Not implemented.')
73}
74
75export {
76 up,
77 down
78}
diff --git a/server/initializers/migrations/0770-actor-preferred-username.ts b/server/initializers/migrations/0770-actor-preferred-username.ts
deleted file mode 100644
index 217813f7f..000000000
--- a/server/initializers/migrations/0770-actor-preferred-username.ts
+++ /dev/null
@@ -1,44 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const { transaction } = utils
10
11 await utils.sequelize.query('drop index if exists "actor_preferred_username"', { transaction })
12 await utils.sequelize.query('drop index if exists "actor_preferred_username_server_id"', { transaction })
13
14 await utils.sequelize.query(
15 'DELETE FROM "actor" v1 USING (' +
16 'SELECT MIN(id) as id, lower("preferredUsername") AS "lowerPreferredUsername", "serverId" ' +
17 'FROM "actor" ' +
18 'GROUP BY "lowerPreferredUsername", "serverId" HAVING COUNT(*) > 1 AND "serverId" IS NOT NULL' +
19 ') v2 ' +
20 'WHERE lower(v1."preferredUsername") = v2."lowerPreferredUsername" AND v1."serverId" = v2."serverId" AND v1.id <> v2.id',
21 { transaction }
22 )
23
24 await utils.sequelize.query(
25 'DELETE FROM "actor" v1 USING (' +
26 'SELECT MIN(id) as id, lower("preferredUsername") AS "lowerPreferredUsername", "serverId" ' +
27 'FROM "actor" ' +
28 'GROUP BY "lowerPreferredUsername", "serverId" HAVING COUNT(*) > 1 AND "serverId" IS NULL' +
29 ') v2 ' +
30 'WHERE lower(v1."preferredUsername") = v2."lowerPreferredUsername" AND v1."serverId" IS NULL AND v1.id <> v2.id',
31 { transaction }
32 )
33}
34
35async function down (utils: {
36 queryInterface: Sequelize.QueryInterface
37 transaction: Sequelize.Transaction
38}) {
39}
40
41export {
42 up,
43 down
44}
diff --git a/server/initializers/migrations/0775-add-user-is-email-public.ts b/server/initializers/migrations/0775-add-user-is-email-public.ts
deleted file mode 100644
index 74dee192c..000000000
--- a/server/initializers/migrations/0775-add-user-is-email-public.ts
+++ /dev/null
@@ -1,25 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8
9 const data = {
10 type: Sequelize.BOOLEAN,
11 allowNull: false,
12 defaultValue: false
13 }
14
15 await utils.queryInterface.addColumn('user', 'emailPublic', data)
16}
17
18function down (options) {
19 throw new Error('Not implemented.')
20}
21
22export {
23 up,
24 down
25}
diff --git a/server/initializers/migrations/0780-notification-registration.ts b/server/initializers/migrations/0780-notification-registration.ts
deleted file mode 100644
index 5f1e0d2ea..000000000
--- a/server/initializers/migrations/0780-notification-registration.ts
+++ /dev/null
@@ -1,30 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 const { transaction } = utils
9
10 {
11 await utils.sequelize.query('DELETE FROM "userNotification" WHERE type = 20 AND "userRegistrationId" IS NULL', { transaction })
12 }
13
14 {
15 await utils.sequelize.query(
16 'ALTER TABLE "userNotification" DROP CONSTRAINT "userNotification_userRegistrationId_fkey", ' +
17 'ADD CONSTRAINT "userNotification_userRegistrationId_fkey" ' +
18 'FOREIGN KEY ("userRegistrationId") REFERENCES "userRegistration" ("id") ON DELETE CASCADE ON UPDATE CASCADE',
19 { transaction })
20 }
21}
22
23function down (options) {
24 throw new Error('Not implemented.')
25}
26
27export {
28 up,
29 down
30}
diff --git a/server/initializers/migrations/0785-video-password-protection.ts b/server/initializers/migrations/0785-video-password-protection.ts
deleted file mode 100644
index 1d85f4489..000000000
--- a/server/initializers/migrations/0785-video-password-protection.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 {
9 const query = `
10 CREATE TABLE IF NOT EXISTS "videoPassword" (
11 "id" SERIAL,
12 "password" VARCHAR(255) NOT NULL,
13 "videoId" INTEGER NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
14 "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
15 "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
16 PRIMARY KEY ("id")
17 );
18 `
19
20 await utils.sequelize.query(query, { transaction : utils.transaction })
21 }
22}
23
24function down (options) {
25 throw new Error('Not implemented.')
26}
27
28export {
29 up,
30 down
31}
diff --git a/server/initializers/migrations/0790-thumbnail-disk.ts b/server/initializers/migrations/0790-thumbnail-disk.ts
deleted file mode 100644
index 0824c042e..000000000
--- a/server/initializers/migrations/0790-thumbnail-disk.ts
+++ /dev/null
@@ -1,47 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 const { transaction } = utils
9
10 {
11 const data = {
12 type: Sequelize.BOOLEAN,
13 allowNull: true,
14 defaultValue: true
15 }
16
17 await utils.queryInterface.addColumn('thumbnail', 'onDisk', data, { transaction })
18 }
19
20 {
21 // Remote previews are not on the disk
22 await utils.sequelize.query(
23 'UPDATE "thumbnail" SET "onDisk" = FALSE ' +
24 'WHERE "type" = 2 AND "videoId" NOT IN (SELECT "id" FROM "video" WHERE "remote" IS FALSE)',
25 { transaction }
26 )
27 }
28
29 {
30 const data = {
31 type: Sequelize.BOOLEAN,
32 allowNull: false,
33 defaultValue: null
34 }
35
36 await utils.queryInterface.changeColumn('thumbnail', 'onDisk', data, { transaction })
37 }
38}
39
40function down (options) {
41 throw new Error('Not implemented.')
42}
43
44export {
45 up,
46 down
47}
diff --git a/server/initializers/migrations/0795-duplicate-runner-name.ts b/server/initializers/migrations/0795-duplicate-runner-name.ts
deleted file mode 100644
index dd15a8157..000000000
--- a/server/initializers/migrations/0795-duplicate-runner-name.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 const { transaction } = utils
9
10 const query = 'DELETE FROM "runner" r1 ' +
11 'USING (SELECT MIN(id) as id, "name" FROM "runner" GROUP BY "name" HAVING COUNT(*) > 1) r2 ' +
12 'WHERE r1."name" = r2."name" AND r1.id <> r2.id'
13
14 await utils.sequelize.query(query, { transaction })
15}
16
17function down (options) {
18 throw new Error('Not implemented.')
19}
20
21export {
22 up,
23 down
24}
diff --git a/server/initializers/migrations/0800-video-replace-file.ts b/server/initializers/migrations/0800-video-replace-file.ts
deleted file mode 100644
index f924a4d92..000000000
--- a/server/initializers/migrations/0800-video-replace-file.ts
+++ /dev/null
@@ -1,38 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 const { transaction } = utils
9
10 {
11 const query = 'DELETE FROM "videoSource" WHERE "videoId" IS NULL'
12 await utils.sequelize.query(query, { transaction })
13 }
14
15 {
16 const query = 'ALTER TABLE "videoSource" ALTER COLUMN "videoId" SET NOT NULL'
17 await utils.sequelize.query(query, { transaction })
18 }
19
20 {
21 const data = {
22 type: Sequelize.DATE,
23 allowNull: true,
24 defaultValue: null
25 }
26
27 await utils.queryInterface.addColumn('video', 'inputFileUpdatedAt', data, { transaction })
28 }
29}
30
31function down (options) {
32 throw new Error('Not implemented.')
33}
34
35export {
36 up,
37 down
38}
diff --git a/server/initializers/migrator.ts b/server/initializers/migrator.ts
deleted file mode 100644
index 7ac20127e..000000000
--- a/server/initializers/migrator.ts
+++ /dev/null
@@ -1,105 +0,0 @@
1import { readdir } from 'fs-extra'
2import { join } from 'path'
3import { QueryTypes } from 'sequelize'
4import { logger } from '../helpers/logger'
5import { LAST_MIGRATION_VERSION } from './constants'
6import { sequelizeTypescript } from './database'
7
8async function migrate () {
9 const tables = await sequelizeTypescript.getQueryInterface().showAllTables()
10
11 // No tables, we don't need to migrate anything
12 // The installer will do that
13 if (tables.length === 0) return
14
15 let actualVersion: number | null = null
16
17 const query = 'SELECT "migrationVersion" FROM "application"'
18 const options = {
19 type: QueryTypes.SELECT as QueryTypes.SELECT
20 }
21
22 const rows = await sequelizeTypescript.query<{ migrationVersion: number }>(query, options)
23 if (rows?.[0]?.migrationVersion) {
24 actualVersion = rows[0].migrationVersion
25 }
26
27 if (actualVersion === null) {
28 await sequelizeTypescript.query('INSERT INTO "application" ("migrationVersion") VALUES (0)')
29 actualVersion = 0
30 }
31
32 // No need migrations, abort
33 if (actualVersion >= LAST_MIGRATION_VERSION) return
34
35 // If there are a new migration scripts
36 logger.info('Begin migrations.')
37
38 const migrationScripts = await getMigrationScripts()
39
40 for (const migrationScript of migrationScripts) {
41 try {
42 await executeMigration(actualVersion, migrationScript)
43 } catch (err) {
44 logger.error('Cannot execute migration %s.', migrationScript.version, { err })
45 process.exit(-1)
46 }
47 }
48
49 logger.info('Migrations finished. New migration version schema: %s', LAST_MIGRATION_VERSION)
50}
51
52// ---------------------------------------------------------------------------
53
54export {
55 migrate
56}
57
58// ---------------------------------------------------------------------------
59
60async function getMigrationScripts () {
61 const files = await readdir(join(__dirname, 'migrations'))
62 const filesToMigrate: {
63 version: string
64 script: string
65 }[] = []
66
67 files
68 .filter(file => file.endsWith('.js'))
69 .forEach(file => {
70 // Filename is something like 'version-blabla.js'
71 const version = file.split('-')[0]
72 filesToMigrate.push({
73 version,
74 script: file
75 })
76 })
77
78 return filesToMigrate
79}
80
81async function executeMigration (actualVersion: number, entity: { version: string, script: string }) {
82 const versionScript = parseInt(entity.version, 10)
83
84 // Do not execute old migration scripts
85 if (versionScript <= actualVersion) return undefined
86
87 // Load the migration module and run it
88 const migrationScriptName = entity.script
89 logger.info('Executing %s migration script.', migrationScriptName)
90
91 const migrationScript = require(join(__dirname, 'migrations', migrationScriptName))
92
93 return sequelizeTypescript.transaction(async t => {
94 const options = {
95 transaction: t,
96 queryInterface: sequelizeTypescript.getQueryInterface(),
97 sequelize: sequelizeTypescript
98 }
99
100 await migrationScript.up(options)
101
102 // Update the new migration version
103 await sequelizeTypescript.query('UPDATE "application" SET "migrationVersion" = ' + versionScript, { transaction: t })
104 })
105}