diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 13b43306b..0a032df12 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html @@ -108,6 +108,11 @@ i18n-labelText labelText="Video import with HTTP enabled" > + +
Administrator
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index bc5ce6e5d..fd6784415 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts @@ -72,6 +72,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { signupEnabled: null, signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT, importVideosHttpEnabled: null, + importVideosTorrentEnabled: null, adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS, @@ -189,6 +190,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { videos: { http: { enabled: this.form.value['importVideosHttpEnabled'] + }, + torrent: { + enabled: this.form.value['importVideosTorrentEnabled'] } } } @@ -231,7 +235,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { transcodingEnabled: this.customConfig.transcoding.enabled, customizationJavascript: this.customConfig.instance.customizations.javascript, customizationCSS: this.customConfig.instance.customizations.css, - importVideosHttpEnabled: this.customConfig.import.videos.http.enabled + importVideosHttpEnabled: this.customConfig.import.videos.http.enabled, + importVideosTorrentEnabled: this.customConfig.import.videos.torrent.enabled } for (const resolution of this.resolutions) { diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index ab317f0aa..52b50cbe8 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -73,6 +73,9 @@ export class ServerService { videos: { http: { enabled: false + }, + torrent: { + enabled: false } } } diff --git a/client/src/app/videos/+video-edit/video-add.component.scss b/client/src/app/videos/+video-edit/video-add.component.scss index 443361f50..c1f96cc37 100644 --- a/client/src/app/videos/+video-edit/video-add.component.scss +++ b/client/src/app/videos/+video-edit/video-add.component.scss @@ -50,6 +50,7 @@ $background-color: #F7F7F7; border-radius: 3px; width: 100%; min-height: 440px; + padding-bottom: 20px; display: flex; justify-content: center; align-items: center; diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index 7d360598d..1a9247dbe 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts @@ -40,6 +40,6 @@ export class VideoAddComponent implements CanComponentDeactivate { } isVideoImportTorrentEnabled () { - return this.serverService.getConfig().import.videos.http.enabled + return this.serverService.getConfig().import.videos.torrent.enabled } } diff --git a/config/default.yaml b/config/default.yaml index 5fa7e5945..60da192b4 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -97,6 +97,8 @@ import: videos: http: # Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html enabled: false + torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file) + enabled: false instance: name: 'PeerTube' diff --git a/config/production.yaml.example b/config/production.yaml.example index 635a41e9e..9e8b57829 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -111,6 +111,8 @@ import: videos: http: # Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html enabled: false + torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file) + enabled: false # Instance settings instance: diff --git a/config/test.yaml b/config/test.yaml index 3c51785f2..879b6bdd4 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -44,6 +44,8 @@ import: videos: http: enabled: true + torrent: + enabled: true instance: default_nsfw_policy: 'display' \ No newline at end of file diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 950a1498e..6f05c33db 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -9,7 +9,7 @@ import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers' import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' import { customConfigUpdateValidator } from '../../middlewares/validators/config' import { ClientHtml } from '../../lib/client-html' -import { CustomConfigAuditView, auditLoggerFactory } from '../../helpers/audit-logger' +import { auditLoggerFactory, CustomConfigAuditView } from '../../helpers/audit-logger' const packageJSON = require('../../../../package.json') const configRouter = express.Router() @@ -69,6 +69,9 @@ async function getConfig (req: express.Request, res: express.Response, next: exp videos: { http: { enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED + }, + torrent: { + enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED } } }, @@ -237,6 +240,9 @@ function customConfig (): CustomConfig { videos: { http: { enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED + }, + torrent: { + enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED } } } diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 879ba3f91..36bf0e0fe 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -196,7 +196,7 @@ async function getUserVideos (req: express.Request, res: express.Response, next: async function getUserVideoImports (req: express.Request, res: express.Response, next: express.NextFunction) { const user = res.locals.oauth.token.User as UserModel const resultList = await VideoImportModel.listUserVideoImportsForApi( - user.Account.id, + user.id, req.query.start as number, req.query.count as number, req.query.sort diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index df151e79d..94dafcdbd 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts @@ -61,12 +61,13 @@ export { function addVideoImport (req: express.Request, res: express.Response) { if (req.body.targetUrl) return addYoutubeDLImport(req, res) - const file = req.files['torrentfile'][0] + const file = req.files && req.files['torrentfile'] ? req.files['torrentfile'][0] : undefined if (req.body.magnetUri || file) return addTorrentImport(req, res, file) } async function addTorrentImport (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) { const body: VideoImportCreate = req.body + const user = res.locals.oauth.token.User let videoName: string let torrentName: string @@ -100,7 +101,8 @@ async function addTorrentImport (req: express.Request, res: express.Response, to const videoImportAttributes = { magnetUri, torrentName, - state: VideoImportState.PENDING + state: VideoImportState.PENDING, + userId: user.id } const videoImport: VideoImportModel = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes) @@ -120,6 +122,7 @@ async function addTorrentImport (req: express.Request, res: express.Response, to async function addYoutubeDLImport (req: express.Request, res: express.Response) { const body: VideoImportCreate = req.body const targetUrl = body.targetUrl + const user = res.locals.oauth.token.User let youtubeDLInfo: YoutubeDLInfo try { @@ -140,7 +143,8 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) const tags = body.tags || youtubeDLInfo.tags const videoImportAttributes = { targetUrl, - state: VideoImportState.PENDING + state: VideoImportState.PENDING, + userId: user.id } const videoImport: VideoImportModel = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes) diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index cf7cd3d74..80eb3f1e7 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -15,7 +15,7 @@ let config: IConfig = require('config') // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 245 +const LAST_MIGRATION_VERSION = 240 // --------------------------------------------------------------------------- @@ -211,6 +211,9 @@ const CONFIG = { VIDEOS: { HTTP: { get ENABLED () { return config.get('import.videos.http.enabled') } + }, + TORRENT: { + get ENABLED () { return config.get('import.videos.torrent.enabled') } } } }, diff --git a/server/initializers/migrations/0245-import-magnet.ts b/server/initializers/migrations/0245-import-magnet.ts deleted file mode 100644 index 87603b006..000000000 --- a/server/initializers/migrations/0245-import-magnet.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as Sequelize from 'sequelize' -import { Migration } from '../../models/migrations' -import { CONSTRAINTS_FIELDS } from '../index' - -async function up (utils: { - transaction: Sequelize.Transaction - queryInterface: Sequelize.QueryInterface - sequelize: Sequelize.Sequelize -}): Promise { - { - const data = { - type: Sequelize.STRING, - allowNull: true, - defaultValue: null - } as Migration.String - await utils.queryInterface.changeColumn('videoImport', 'targetUrl', data) - } - - { - const data = { - type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max), - allowNull: true, - defaultValue: null - } - await utils.queryInterface.addColumn('videoImport', 'magnetUri', data) - } - - { - const data = { - type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_NAME.max), - allowNull: true, - defaultValue: null - } - await utils.queryInterface.addColumn('videoImport', 'torrentName', data) - } -} - -function down (options) { - throw new Error('Not implemented.') -} - -export { up, down } diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index fd61aecad..28a03d19e 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts @@ -114,16 +114,21 @@ async function processFile (downloader: () => Promise, videoImport: Vide tempVideoPath = await downloader() // Get information about this video + const { size } = await statPromise(tempVideoPath) + const isAble = await videoImport.User.isAbleToUploadVideo({ size }) + if (isAble === false) { + throw new Error('The user video quota is exceeded with this video to import.') + } + const { videoFileResolution } = await getVideoFileResolution(tempVideoPath) const fps = await getVideoFileFPS(tempVideoPath) - const stats = await statPromise(tempVideoPath) const duration = await getDurationFromVideoFile(tempVideoPath) // Create video file object in database const videoFileData = { extname: extname(tempVideoPath), resolution: videoFileResolution, - size: stats.size, + size, fps, videoId: videoImport.videoId } diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index 9d303eee2..f3f257d57 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts @@ -25,6 +25,7 @@ const customConfigUpdateValidator = [ body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'), body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'), body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), + body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body }) diff --git a/server/middlewares/validators/video-imports.ts b/server/middlewares/validators/video-imports.ts index c03cf2e4d..9ac739101 100644 --- a/server/middlewares/validators/video-imports.ts +++ b/server/middlewares/validators/video-imports.ts @@ -33,21 +33,28 @@ const videoImportAddValidator = getCommonVideoAttributes().concat([ logger.debug('Checking videoImportAddValidator parameters', { parameters: req.body }) const user = res.locals.oauth.token.User + const torrentFile = req.files && req.files['torrentfile'] ? req.files['torrentfile'][0] : undefined if (areValidationErrors(req, res)) return cleanUpReqFiles(req) - if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true) { + if (req.body.targetUrl && CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true) { cleanUpReqFiles(req) return res.status(409) - .json({ error: 'Import is not enabled on this instance.' }) + .json({ error: 'HTTP import is not enabled on this instance.' }) .end() } + if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) { + cleanUpReqFiles(req) + return res.status(409) + .json({ error: 'Torrent/magnet URI import is not enabled on this instance.' }) + .end() + } + if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) // Check we have at least 1 required param - const file = req.files['torrentfile'][0] - if (!req.body.targetUrl && !req.body.magnetUri && !file) { + if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) { cleanUpReqFiles(req) return res.status(400) diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 1165285ea..1b1fc5ee8 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -295,7 +295,7 @@ export class UserModel extends Model { return json } - isAbleToUploadVideo (videoFile: Express.Multer.File) { + isAbleToUploadVideo (videoFile: { size: number }) { if (this.videoQuota === -1) return Promise.resolve(true) return UserModel.getOriginalVideoFileTotalFromUser(this) diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index d6c02e5ac..b794d8324 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts @@ -15,34 +15,21 @@ import { } from 'sequelize-typescript' import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers' import { getSort, throwIfNotValid } from '../utils' -import { VideoModel } from './video' +import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports' import { VideoImport, VideoImportState } from '../../../shared' -import { VideoChannelModel } from './video-channel' -import { AccountModel } from '../account/account' -import { TagModel } from './tag' import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' +import { UserModel } from '../account/user' @DefaultScope({ include: [ { - model: () => VideoModel, - required: false, - include: [ - { - model: () => VideoChannelModel, - required: true, - include: [ - { - model: () => AccountModel, - required: true - } - ] - }, - { - model: () => TagModel - } - ] + model: () => UserModel.unscoped(), + required: true + }, + { + model: () => VideoModel.scope([ VideoModelScopeNames.WITH_ACCOUNT_DETAILS, VideoModelScopeNames.WITH_TAGS]), + required: false } ] }) @@ -53,6 +40,9 @@ import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' { fields: [ 'videoId' ], unique: true + }, + { + fields: [ 'userId' ] } ] }) @@ -91,6 +81,18 @@ export class VideoImportModel extends Model { @Column(DataType.TEXT) error: string + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade' + }) + User: UserModel + @ForeignKey(() => VideoModel) @Column videoId: number @@ -116,41 +118,24 @@ export class VideoImportModel extends Model { return VideoImportModel.findById(id) } - static listUserVideoImportsForApi (accountId: number, start: number, count: number, sort: string) { + static listUserVideoImportsForApi (userId: number, start: number, count: number, sort: string) { const query = { distinct: true, + include: [ + { + model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query + required: true + } + ], offset: start, limit: count, order: getSort(sort), - include: [ - { - model: VideoModel, - required: false, - include: [ - { - model: VideoChannelModel, - required: true, - include: [ - { - model: AccountModel, - required: true, - where: { - id: accountId - } - } - ] - }, - { - model: TagModel, - required: false - } - ] - } - ] + where: { + userId + } } - return VideoImportModel.unscoped() - .findAndCountAll(query) + return VideoImportModel.findAndCountAll(query) .then(({ rows, count }) => { return { data: rows, diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 2742e26de..b26dfa252 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts @@ -65,6 +65,9 @@ describe('Test config API validators', function () { videos: { http: { enabled: false + }, + torrent: { + enabled: false } } } diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index b65061a5d..f9805b6ea 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts @@ -45,6 +45,7 @@ function checkInitialConfig (data: CustomConfig) { expect(data.transcoding.resolutions['720p']).to.be.true expect(data.transcoding.resolutions['1080p']).to.be.true expect(data.import.videos.http.enabled).to.be.true + expect(data.import.videos.torrent.enabled).to.be.true } function checkUpdatedConfig (data: CustomConfig) { @@ -72,6 +73,7 @@ function checkUpdatedConfig (data: CustomConfig) { expect(data.transcoding.resolutions['720p']).to.be.false expect(data.transcoding.resolutions['1080p']).to.be.false expect(data.import.videos.http.enabled).to.be.false + expect(data.import.videos.torrent.enabled).to.be.false } describe('Test config', function () { @@ -167,6 +169,9 @@ describe('Test config', function () { videos: { http: { enabled: false + }, + torrent: { + enabled: false } } } diff --git a/server/tests/utils/server/config.ts b/server/tests/utils/server/config.ts index e21614282..d6ac3ef8a 100644 --- a/server/tests/utils/server/config.ts +++ b/server/tests/utils/server/config.ts @@ -97,6 +97,9 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) { videos: { http: { enabled: false + }, + torrent: { + enabled: false } } } diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index 46320435d..d70c757b6 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts @@ -60,6 +60,9 @@ export interface CustomConfig { videos: { http: { enabled: boolean + }, + torrent: { + enabled: boolean } } } diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index 2cafedbbc..8cb087234 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts @@ -28,6 +28,9 @@ export interface ServerConfig { http: { enabled: boolean } + torrent: { + enabled: boolean + } } }