Implement user import/export in server
This commit is contained in:
parent
4d63e6f577
commit
8573e5a80a
|
@ -1,4 +1,4 @@
|
|||
# /!\ YOU SHOULD NOT UPDATE THIS FILE, USE production.yaml instead /!\ #
|
||||
# /!\ DO NOT UPDATE THIS FILE, USE production.yaml instead /!\ #
|
||||
|
||||
listen:
|
||||
hostname: '127.0.0.1'
|
||||
|
@ -222,12 +222,16 @@ object_storage:
|
|||
# Useful when you want to use a CDN/external proxy
|
||||
base_url: '' # Example: 'https://mirror.example.com'
|
||||
|
||||
# Same settings but for web videos
|
||||
web_videos:
|
||||
bucket_name: 'web-videos'
|
||||
prefix: ''
|
||||
base_url: ''
|
||||
|
||||
user_exports:
|
||||
bucket_name: 'user-exports'
|
||||
prefix: ''
|
||||
base_url: ''
|
||||
|
||||
log:
|
||||
level: 'info' # 'debug' | 'info' | 'warn' | 'error'
|
||||
|
||||
|
@ -482,11 +486,14 @@ user:
|
|||
videos:
|
||||
# Enable or disable video history by default for new users.
|
||||
enabled: true
|
||||
# Default value of maximum video bytes the user can upload (does not take into account transcoded files)
|
||||
|
||||
# Default value of maximum video bytes the user can upload
|
||||
# Does not take into account transcoded files or account export archives (that can include user uploaded files)
|
||||
# Byte format is supported ("1GB" etc)
|
||||
# -1 == unlimited
|
||||
video_quota: -1
|
||||
video_quota_daily: -1
|
||||
|
||||
default_channel_name: 'Main $1 channel' # The placeholder $1 is used to represent the user's username
|
||||
|
||||
video_channels:
|
||||
|
@ -707,6 +714,24 @@ import:
|
|||
# Max number of videos to import when the user asks for full sync
|
||||
full_sync_videos_limit: 1000
|
||||
|
||||
users:
|
||||
# Video quota is checked on import so the user doesn't upload a too big archive file
|
||||
# Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import
|
||||
enabled: true
|
||||
|
||||
export:
|
||||
users:
|
||||
# Allow users to export their PeerTube data in a .zip for backup or re-import
|
||||
# Only one export at a time is allowed per user
|
||||
enabled: true
|
||||
|
||||
# Max size of the current user quota to accept or not the export
|
||||
# Goal of this setting is to not store too big archive file on your server disk
|
||||
max_user_video_quota: 10GB
|
||||
|
||||
# How long PeerTube should keep the user export
|
||||
export_expiration: '2 days'
|
||||
|
||||
auto_blacklist:
|
||||
# New videos automatically blacklisted so moderators can review before publishing
|
||||
videos:
|
||||
|
@ -867,6 +892,7 @@ client:
|
|||
# By default PeerTube client displays author username
|
||||
prefer_author_display_name: false
|
||||
display_author_avatar: false
|
||||
|
||||
resumable_upload:
|
||||
# Max size of upload chunks, e.g. '90MB'
|
||||
# If null, it will be calculated based on network speed
|
||||
|
|
|
@ -220,12 +220,16 @@ object_storage:
|
|||
# Useful when you want to use a CDN/external proxy
|
||||
base_url: '' # Example: 'https://mirror.example.com'
|
||||
|
||||
# Same settings but for web videos
|
||||
web_videos:
|
||||
bucket_name: 'web-videos'
|
||||
prefix: ''
|
||||
base_url: ''
|
||||
|
||||
user_exports:
|
||||
bucket_name: 'user-exports'
|
||||
prefix: ''
|
||||
base_url: ''
|
||||
|
||||
log:
|
||||
level: 'info' # 'debug' | 'info' | 'warn' | 'error'
|
||||
|
||||
|
@ -492,11 +496,14 @@ user:
|
|||
videos:
|
||||
# Enable or disable video history by default for new users.
|
||||
enabled: true
|
||||
# Default value of maximum video bytes the user can upload (does not take into account transcoded files)
|
||||
|
||||
# Default value of maximum video bytes the user can upload
|
||||
# Does not take into account transcoded files or account export archives (that can include user uploaded files)
|
||||
# Byte format is supported ("1GB" etc)
|
||||
# -1 == unlimited
|
||||
video_quota: -1
|
||||
video_quota_daily: -1
|
||||
|
||||
default_channel_name: 'Main $1 channel' # The placeholder $1 is used to represent the user's username
|
||||
|
||||
video_channels:
|
||||
|
@ -717,6 +724,24 @@ import:
|
|||
# Max number of videos to import when the user asks for full sync
|
||||
full_sync_videos_limit: 1000
|
||||
|
||||
users:
|
||||
# Video quota is checked on import so the user doesn't upload a too big archive file
|
||||
# Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import
|
||||
enabled: true
|
||||
|
||||
export:
|
||||
users:
|
||||
# Allow users to export their PeerTube data in a .zip for backup or re-import
|
||||
# Only one export at a time is allowed per user
|
||||
enabled: true
|
||||
|
||||
# Max size of the current user quota to accept or not the export
|
||||
# Goal of this setting is to not store too big archive file on your server disk
|
||||
max_user_video_quota: 10GB
|
||||
|
||||
# How long PeerTube should keep the user export
|
||||
export_expiration: '2 days'
|
||||
|
||||
auto_blacklist:
|
||||
# New videos automatically blacklisted so moderators can review before publishing
|
||||
videos:
|
||||
|
@ -877,6 +902,7 @@ client:
|
|||
# By default PeerTube client displays author username
|
||||
prefer_author_display_name: false
|
||||
display_author_avatar: false
|
||||
|
||||
resumable_upload:
|
||||
# Max size of upload chunks, e.g. '90MB'
|
||||
# If null, it will be calculated based on network speed
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
"@peertube/http-signature": "^1.7.0",
|
||||
"@smithy/node-http-handler": "^2.1.7",
|
||||
"@uploadx/core": "^6.0.0",
|
||||
"archiver": "^6.0.1",
|
||||
"async-mutex": "^0.4.0",
|
||||
"bcrypt": "5.1.1",
|
||||
"bencode": "^4.0.0",
|
||||
|
@ -142,6 +143,7 @@
|
|||
"jimp": "^0.22.4",
|
||||
"js-yaml": "^4.0.0",
|
||||
"jsonld": "~8.3.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lru-cache": "^10.0.1",
|
||||
"magnet-uri": "^7.0.5",
|
||||
|
@ -178,11 +180,13 @@
|
|||
"webfinger.js": "^2.6.6",
|
||||
"webtorrent": "^2.1.27",
|
||||
"winston": "3.11.0",
|
||||
"ws": "^8.0.0"
|
||||
"ws": "^8.0.0",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@peertube/maildev": "^1.2.0",
|
||||
"@peertube/resolve-tspaths": "^0.8.14",
|
||||
"@types/archiver": "^6.0.2",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bencode": "^2.0.0",
|
||||
"@types/bluebird": "^3.5.33",
|
||||
|
@ -197,6 +201,7 @@
|
|||
"@types/fluent-ffmpeg": "^2.1.16",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/jsonld": "^1.5.9",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/magnet-uri": "^5.1.1",
|
||||
"@types/maildev": "^0.0.4",
|
||||
|
@ -212,6 +217,7 @@
|
|||
"@types/validator": "^13.9.0",
|
||||
"@types/webtorrent": "^0.109.0",
|
||||
"@types/ws": "^8.2.0",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
||||
"autocannon": "^7.0.4",
|
||||
"chai": "^4.1.1",
|
||||
|
@ -228,6 +234,7 @@
|
|||
"eslint-plugin-promise": "^6.0.0",
|
||||
"fast-xml-parser": "^4.0.0-beta.8",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"jszip": "^3.10.1",
|
||||
"mocha": "^10.0.0",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
|
|
|
@ -19,6 +19,18 @@ function removeQueryParams (url: string) {
|
|||
return objUrl.toString()
|
||||
}
|
||||
|
||||
function queryParamsToObject (entries: any) {
|
||||
const result: { [ id: string ]: string | number | boolean } = {}
|
||||
|
||||
for (const [ key, value ] of entries) {
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildPlaylistLink (playlist: Pick<VideoPlaylist, 'shortUUID'>, base?: string) {
|
||||
return (base ?? window.location.origin) + buildPlaylistWatchPath(playlist)
|
||||
}
|
||||
|
@ -123,6 +135,7 @@ function decoratePlaylistLink (options: {
|
|||
export {
|
||||
addQueryParams,
|
||||
removeQueryParams,
|
||||
queryParamsToObject,
|
||||
|
||||
buildPlaylistLink,
|
||||
buildVideoLink,
|
||||
|
|
|
@ -18,7 +18,7 @@ export interface ActivityPubActor {
|
|||
sharedInbox: string
|
||||
}
|
||||
summary: string
|
||||
attributedTo: ActivityPubAttributedTo[]
|
||||
attributedTo?: ActivityPubAttributedTo[]
|
||||
|
||||
support?: string
|
||||
publicKey: {
|
||||
|
@ -31,4 +31,8 @@ export interface ActivityPubActor {
|
|||
icon?: ActivityIconObject | ActivityIconObject[]
|
||||
|
||||
published?: string
|
||||
|
||||
// For export
|
||||
likes?: string
|
||||
dislikes?: string
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Activity } from './activity.js'
|
||||
|
||||
export interface ActivityPubCollection {
|
||||
'@context': string[]
|
||||
'@context': any[]
|
||||
type: 'Collection' | 'CollectionPage'
|
||||
totalItems: number
|
||||
partOf?: string
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export interface ActivityPubOrderedCollection<T> {
|
||||
'@context': string[]
|
||||
id: string
|
||||
|
||||
'@context': any[]
|
||||
type: 'OrderedCollection' | 'OrderedCollectionPage'
|
||||
totalItems: number
|
||||
orderedItems: T[]
|
||||
|
|
|
@ -59,6 +59,16 @@ export interface VideoObject {
|
|||
|
||||
to?: string[]
|
||||
cc?: string[]
|
||||
|
||||
// For export
|
||||
attachment?: {
|
||||
type: 'Video'
|
||||
url: string
|
||||
mediaType: string
|
||||
height: number
|
||||
size: number
|
||||
fps: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface ActivityPubStoryboard {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export const FileStorage = {
|
||||
FILE_SYSTEM: 0,
|
||||
OBJECT_STORAGE: 1
|
||||
} as const
|
||||
|
||||
export type FileStorageType = typeof FileStorage[keyof typeof FileStorage]
|
|
@ -1 +1,2 @@
|
|||
export * from './file-storage.enum.js'
|
||||
export * from './result-list.model.js'
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export * from './peertube-export-format/index.js'
|
||||
export * from './user-export-request-result.model.js'
|
||||
export * from './user-export-request.model.js'
|
||||
export * from './user-export-state.enum.js'
|
||||
export * from './user-export.model.js'
|
||||
export * from './user-import.model.js'
|
||||
export * from './user-import-state.enum.js'
|
||||
export * from './user-import-result.model.js'
|
||||
export * from './user-import-upload-result.model.js'
|
|
@ -0,0 +1,18 @@
|
|||
import { UserActorImageJSON } from './actor-export.model.js'
|
||||
|
||||
export interface AccountExportJSON {
|
||||
url: string
|
||||
|
||||
name: string
|
||||
displayName: string
|
||||
description: string
|
||||
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
|
||||
avatars: UserActorImageJSON[]
|
||||
|
||||
archiveFiles: {
|
||||
avatar: string | null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export interface UserActorImageJSON {
|
||||
width: number
|
||||
url: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export interface BlocklistExportJSON {
|
||||
instances: {
|
||||
host: string
|
||||
}[]
|
||||
|
||||
actors: {
|
||||
handle: string
|
||||
}[]
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { UserActorImageJSON } from './actor-export.model.js'
|
||||
|
||||
export interface ChannelExportJSON {
|
||||
channels: {
|
||||
url: string
|
||||
|
||||
name: string
|
||||
displayName: string
|
||||
description: string
|
||||
support: string
|
||||
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
|
||||
avatars: UserActorImageJSON[]
|
||||
banners: UserActorImageJSON[]
|
||||
|
||||
archiveFiles: {
|
||||
avatar: string | null
|
||||
banner: string | null
|
||||
}
|
||||
}[]
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
export interface CommentsExportJSON {
|
||||
comments: {
|
||||
url: string
|
||||
text: string
|
||||
createdAt: string
|
||||
videoUrl: string
|
||||
|
||||
inReplyToCommentUrl?: string
|
||||
|
||||
archiveFiles?: never
|
||||
}[]
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export interface DislikesExportJSON {
|
||||
dislikes: {
|
||||
videoUrl: string
|
||||
createdAt: string
|
||||
|
||||
archiveFiles?: never
|
||||
}[]
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export interface FollowersExportJSON {
|
||||
followers: {
|
||||
handle: string
|
||||
createdAt: string
|
||||
targetHandle: string
|
||||
|
||||
archiveFiles?: never
|
||||
}[]
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export interface FollowingExportJSON {
|
||||
following: {
|
||||
handle: string
|
||||
targetHandle: string
|
||||
createdAt: string
|
||||
|
||||
archiveFiles?: never
|
||||
}[]
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
export * from './account-export.model.js'
|
||||
export * from './actor-export.model.js'
|
||||
export * from './blocklist-export.model.js'
|
||||
export * from './channel-export.model.js'
|
||||
export * from './comments-export.model.js'
|
||||
export * from './dislikes-export.model.js'
|
||||
export * from './followers-export.model.js'
|
||||
export * from './following-export.model.js'
|
||||
export * from './likes-export.model.js'
|
||||
export * from './user-settings-export.model.js'
|
||||
export * from './video-export.model.js'
|
||||
export * from './video-playlists-export.model.js'
|
|
@ -0,0 +1,8 @@
|
|||
export interface LikesExportJSON {
|
||||
likes: {
|
||||
videoUrl: string
|
||||
createdAt: string
|
||||
|
||||
archiveFiles?: never
|
||||
}[]
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { UserNotificationSetting } from '../../users/user-notification-setting.model.js'
|
||||
import { NSFWPolicyType } from '../../videos/nsfw-policy.type.js'
|
||||
|
||||
export interface UserSettingsExportJSON {
|
||||
email: string
|
||||
|
||||
emailPublic: boolean
|
||||
nsfwPolicy: NSFWPolicyType
|
||||
|
||||
autoPlayVideo: boolean
|
||||
autoPlayNextVideo: boolean
|
||||
autoPlayNextVideoPlaylist: boolean
|
||||
|
||||
p2pEnabled: boolean
|
||||
|
||||
videosHistoryEnabled: boolean
|
||||
videoLanguages: string[]
|
||||
|
||||
theme: string
|
||||
|
||||
createdAt: Date
|
||||
|
||||
notificationSettings: UserNotificationSetting
|
||||
|
||||
archiveFiles?: never
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import {
|
||||
LiveVideoLatencyModeType,
|
||||
VideoPrivacyType,
|
||||
VideoStateType,
|
||||
VideoStreamingPlaylistType_Type
|
||||
} from '../../videos/index.js'
|
||||
|
||||
export interface VideoExportJSON {
|
||||
videos: {
|
||||
uuid: string
|
||||
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
publishedAt: string
|
||||
originallyPublishedAt: string
|
||||
|
||||
name: string
|
||||
category: number
|
||||
licence: number
|
||||
language: string
|
||||
tags: string[]
|
||||
|
||||
privacy: VideoPrivacyType
|
||||
passwords: string[]
|
||||
|
||||
duration: number
|
||||
|
||||
description: string
|
||||
support: string
|
||||
|
||||
isLive: boolean
|
||||
live?: {
|
||||
saveReplay: boolean
|
||||
permanentLive: boolean
|
||||
latencyMode: LiveVideoLatencyModeType
|
||||
streamKey: string
|
||||
|
||||
replaySettings?: {
|
||||
privacy: VideoPrivacyType
|
||||
}
|
||||
}
|
||||
|
||||
url: string
|
||||
|
||||
thumbnailUrl: string
|
||||
previewUrl: string
|
||||
|
||||
views: number
|
||||
|
||||
likes: number
|
||||
dislikes: number
|
||||
|
||||
nsfw: boolean
|
||||
|
||||
commentsEnabled: boolean
|
||||
downloadEnabled: boolean
|
||||
|
||||
channel: {
|
||||
name: string
|
||||
}
|
||||
|
||||
waitTranscoding: boolean
|
||||
state: VideoStateType
|
||||
|
||||
captions: {
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
language: string
|
||||
filename: string
|
||||
fileUrl: string
|
||||
}[]
|
||||
|
||||
files: VideoFileExportJSON[]
|
||||
|
||||
streamingPlaylists: {
|
||||
type: VideoStreamingPlaylistType_Type
|
||||
playlistUrl: string
|
||||
segmentsSha256Url: string
|
||||
files: VideoFileExportJSON[]
|
||||
}[]
|
||||
|
||||
source?: {
|
||||
filename: string
|
||||
}
|
||||
|
||||
archiveFiles: {
|
||||
videoFile: string | null
|
||||
thumbnail: string | null
|
||||
captions: Record<string, string> // The key is the language code
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface VideoFileExportJSON {
|
||||
resolution: number
|
||||
size: number // Bytes
|
||||
fps: number
|
||||
|
||||
torrentUrl: string
|
||||
fileUrl: string
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { VideoPlaylistPrivacyType } from '../../videos/playlist/video-playlist-privacy.model.js'
|
||||
import { VideoPlaylistType_Type } from '../../videos/playlist/video-playlist-type.model.js'
|
||||
|
||||
export interface VideoPlaylistsExportJSON {
|
||||
videoPlaylists: {
|
||||
displayName: string
|
||||
description: string
|
||||
privacy: VideoPlaylistPrivacyType
|
||||
url: string
|
||||
uuid: string
|
||||
|
||||
type: VideoPlaylistType_Type
|
||||
|
||||
channel: {
|
||||
name: string
|
||||
}
|
||||
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
|
||||
thumbnailUrl: string
|
||||
|
||||
elements: {
|
||||
videoUrl: string
|
||||
|
||||
startTimestamp?: number
|
||||
stopTimestamp?: number
|
||||
}[]
|
||||
|
||||
archiveFiles: {
|
||||
thumbnail: string | null
|
||||
}
|
||||
}[]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export interface UserExportRequestResult {
|
||||
export: {
|
||||
id: number
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface UserExportRequest {
|
||||
withVideoFiles: boolean
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export const UserExportState = {
|
||||
PENDING: 1,
|
||||
PROCESSING: 2,
|
||||
COMPLETED: 3,
|
||||
ERRORED: 4
|
||||
} as const
|
||||
|
||||
export type UserExportStateType = typeof UserExportState[keyof typeof UserExportState]
|
|
@ -0,0 +1,18 @@
|
|||
import { UserExportStateType } from './user-export-state.enum.js'
|
||||
|
||||
export interface UserExport {
|
||||
id: number
|
||||
|
||||
state: {
|
||||
id: UserExportStateType
|
||||
label: string
|
||||
}
|
||||
|
||||
// In bytes
|
||||
size: number
|
||||
|
||||
privateDownloadUrl: string
|
||||
|
||||
createdAt: string | Date
|
||||
expiresOn: string | Date
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
type Summary = {
|
||||
success: number
|
||||
duplicates: number
|
||||
errors: number
|
||||
}
|
||||
|
||||
export interface UserImportResultSummary {
|
||||
stats: {
|
||||
blocklist: Summary
|
||||
channels: Summary
|
||||
likes: Summary
|
||||
dislikes: Summary
|
||||
following: Summary
|
||||
videoPlaylists: Summary
|
||||
videos: Summary
|
||||
|
||||
account: Summary
|
||||
userSettings: Summary
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export const UserImportState = {
|
||||
PENDING: 1,
|
||||
PROCESSING: 2,
|
||||
COMPLETED: 3,
|
||||
ERRORED: 4
|
||||
} as const
|
||||
|
||||
export type UserImportStateType = typeof UserImportState[keyof typeof UserImportState]
|
|
@ -0,0 +1,5 @@
|
|||
export interface UserImportUploadResult {
|
||||
userImport: {
|
||||
id: number
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { UserImportStateType } from './user-import-state.enum.js'
|
||||
|
||||
export interface UserImport {
|
||||
id: number
|
||||
state: {
|
||||
id: UserImportStateType
|
||||
label: string
|
||||
}
|
||||
createdAt: string
|
||||
}
|
|
@ -3,6 +3,7 @@ export * from './actors/index.js'
|
|||
export * from './bulk/index.js'
|
||||
export * from './common/index.js'
|
||||
export * from './custom-markup/index.js'
|
||||
export * from './import-export/index.js'
|
||||
export * from './feeds/index.js'
|
||||
export * from './http/index.js'
|
||||
export * from './joinpeertube/index.js'
|
||||
|
|
|
@ -65,6 +65,8 @@ export const serverFilterHookObject = {
|
|||
'filter:api.video.post-import-url.accept.result': true,
|
||||
'filter:api.video.post-import-torrent.accept.result': true,
|
||||
'filter:api.video.update-file.accept.result': true,
|
||||
// PeerTube >= 6.1
|
||||
'filter:api.video.user-import.accept.result': true,
|
||||
// Filter the result of the accept comment (thread or reply) functions
|
||||
// If the functions return false then the user cannot post its comment
|
||||
'filter:api.video-thread.create.accept.result': true,
|
||||
|
@ -75,6 +77,8 @@ export const serverFilterHookObject = {
|
|||
'filter:api.video.import-url.video-attribute.result': true,
|
||||
'filter:api.video.import-torrent.video-attribute.result': true,
|
||||
'filter:api.video.live.video-attribute.result': true,
|
||||
// PeerTube >= 6.1
|
||||
'filter:api.video.user-import.video-attribute.result': true,
|
||||
|
||||
// Filter params/result used to list threads of a specific video
|
||||
// (used by the video watch page)
|
||||
|
|
|
@ -193,10 +193,23 @@ export interface CustomConfig {
|
|||
enabled: boolean
|
||||
}
|
||||
}
|
||||
|
||||
videoChannelSynchronization: {
|
||||
enabled: boolean
|
||||
maxPerUser: number
|
||||
}
|
||||
|
||||
users: {
|
||||
enabled: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export: {
|
||||
users: {
|
||||
enabled: boolean
|
||||
maxUserVideoQuota: number
|
||||
exportExpiration: number
|
||||
}
|
||||
}
|
||||
|
||||
trending: {
|
||||
|
@ -260,5 +273,4 @@ export interface CustomConfig {
|
|||
storyboards: {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,4 +9,5 @@ export interface SendDebugCommand {
|
|||
| 'process-video-viewers'
|
||||
| 'process-video-channel-sync-latest'
|
||||
| 'process-update-videos-scheduler'
|
||||
| 'remove-expired-user-exports'
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ export type JobType =
|
|||
| 'video-transcoding'
|
||||
| 'videos-views-stats'
|
||||
| 'generate-video-storyboard'
|
||||
| 'create-user-export'
|
||||
| 'import-user-archive'
|
||||
|
||||
export interface Job {
|
||||
id: number | string
|
||||
|
@ -302,3 +304,15 @@ export interface GenerateStoryboardPayload {
|
|||
videoUUID: string
|
||||
federate: boolean
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface CreateUserExportPayload {
|
||||
userExportId: number
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ImportUserArchivePayload {
|
||||
userImportId: number
|
||||
}
|
||||
|
|
|
@ -207,9 +207,22 @@ export interface ServerConfig {
|
|||
enabled: boolean
|
||||
}
|
||||
}
|
||||
|
||||
videoChannelSynchronization: {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
users: {
|
||||
enabled:boolean
|
||||
}
|
||||
}
|
||||
|
||||
export: {
|
||||
users: {
|
||||
enabled: boolean
|
||||
exportExpiration: number
|
||||
maxUserVideoQuota: number
|
||||
}
|
||||
}
|
||||
|
||||
autoBlacklist: {
|
||||
|
|
|
@ -54,7 +54,9 @@ export const ServerErrorCode = {
|
|||
VIDEO_REQUIRES_PASSWORD:'video_requires_password',
|
||||
INCORRECT_VIDEO_PASSWORD:'incorrect_video_password',
|
||||
|
||||
VIDEO_ALREADY_BEING_TRANSCODED:'video_already_being_transcoded'
|
||||
VIDEO_ALREADY_BEING_TRANSCODED:'video_already_being_transcoded',
|
||||
|
||||
MAX_USER_VIDEO_QUOTA_EXCEEDED_FOR_USER_EXPORT: 'max_user_video_quota_exceeded_for_user_export'
|
||||
} as const
|
||||
|
||||
/**
|
||||
|
|
|
@ -47,7 +47,10 @@ export const UserRight = {
|
|||
|
||||
MANAGE_REGISTRATIONS: 28,
|
||||
|
||||
MANAGE_RUNNERS: 29
|
||||
MANAGE_RUNNERS: 29,
|
||||
|
||||
MANAGE_USER_EXPORTS: 30,
|
||||
MANAGE_USER_IMPORTS: 31
|
||||
} as const
|
||||
|
||||
export type UserRightType = typeof UserRight[keyof typeof UserRight]
|
||||
|
|
|
@ -29,7 +29,6 @@ export * from './video-rate.type.js'
|
|||
export * from './video-schedule-update.model.js'
|
||||
export * from './video-sort-field.type.js'
|
||||
export * from './video-state.enum.js'
|
||||
export * from './video-storage.enum.js'
|
||||
export * from './video-source.model.js'
|
||||
|
||||
export * from './video-streaming-playlist.model.js'
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
export const VideoStorage = {
|
||||
FILE_SYSTEM: 0,
|
||||
OBJECT_STORAGE: 1
|
||||
} as const
|
||||
|
||||
export type VideoStorageType = typeof VideoStorage[keyof typeof VideoStorage]
|
|
@ -1,4 +1,4 @@
|
|||
import { basename, extname, isAbsolute, join, resolve } from 'path'
|
||||
import { basename, extname, isAbsolute, join, parse, resolve } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
let rootPath: string
|
||||
|
@ -48,3 +48,15 @@ export function buildAbsoluteFixturePath (path: string, customCIPath = false) {
|
|||
|
||||
return join(root(), 'packages', 'tests', 'fixtures', path)
|
||||
}
|
||||
|
||||
export function getFilenameFromUrl (url: string) {
|
||||
return getFilename(new URL(url).pathname)
|
||||
}
|
||||
|
||||
export function getFilename (path: string) {
|
||||
return parse(path).base
|
||||
}
|
||||
|
||||
export function getFilenameWithoutExt (path: string) {
|
||||
return parse(path).name
|
||||
}
|
||||
|
|
|
@ -45,3 +45,5 @@ export type DeepOmitArray<T extends any[], K> = {
|
|||
}
|
||||
|
||||
export type Unpacked<T> = T extends (infer U)[] ? U : T
|
||||
|
||||
export type Awaitable<T> = T | PromiseLike<T>
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
ABUSE_STATES,
|
||||
buildLanguages,
|
||||
RUNNER_JOB_STATES,
|
||||
USER_EXPORT_STATES,
|
||||
USER_REGISTRATION_STATES,
|
||||
VIDEO_CATEGORIES,
|
||||
VIDEO_CHANNEL_SYNC_STATE,
|
||||
|
@ -14,6 +15,7 @@ import {
|
|||
VIDEO_PLAYLIST_PRIVACIES,
|
||||
VIDEO_PLAYLIST_TYPES,
|
||||
VIDEO_PRIVACIES,
|
||||
USER_IMPORT_STATES,
|
||||
VIDEO_STATES
|
||||
} from '@peertube/peertube-server/core/initializers/constants.js'
|
||||
|
||||
|
@ -96,6 +98,8 @@ Object.values(VIDEO_CATEGORIES)
|
|||
.concat(Object.values(ABUSE_STATES))
|
||||
.concat(Object.values(USER_REGISTRATION_STATES))
|
||||
.concat(Object.values(RUNNER_JOB_STATES))
|
||||
.concat(Object.values(USER_EXPORT_STATES))
|
||||
.concat(Object.values(USER_IMPORT_STATES))
|
||||
.concat([
|
||||
'This video does not exist.',
|
||||
'We cannot fetch the video. Please try again later.',
|
||||
|
|
|
@ -355,6 +355,16 @@ function customConfig (): CustomConfig {
|
|||
videoChannelSynchronization: {
|
||||
enabled: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED,
|
||||
maxPerUser: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.MAX_PER_USER
|
||||
},
|
||||
users: {
|
||||
enabled: CONFIG.IMPORT.USERS.ENABLED
|
||||
}
|
||||
},
|
||||
export: {
|
||||
users: {
|
||||
enabled: CONFIG.EXPORT.USERS.ENABLED,
|
||||
exportExpiration: CONFIG.EXPORT.USERS.EXPORT_EXPIRATION,
|
||||
maxUserVideoQuota: CONFIG.EXPORT.USERS.MAX_USER_VIDEO_QUOTA
|
||||
}
|
||||
},
|
||||
trending: {
|
||||
|
|
|
@ -50,7 +50,6 @@ apiRouter.use('/custom-pages', customPageRouter)
|
|||
apiRouter.use('/blocklist', blocklistRouter)
|
||||
apiRouter.use('/runners', runnersRouter)
|
||||
|
||||
// apiRouter.use(apiRateLimiter)
|
||||
apiRouter.use('/ping', pong)
|
||||
apiRouter.use('/*', badRequest)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
runnerJobGetVideoStudioTaskFileValidator,
|
||||
runnerJobGetVideoTranscodingFileValidator
|
||||
} from '@server/middlewares/validators/runners/job-files.js'
|
||||
import { RunnerJobState, VideoStorage } from '@peertube/peertube-models'
|
||||
import { RunnerJobState, FileStorage } from '@peertube/peertube-models'
|
||||
|
||||
const lTags = loggerTagsFactory('api', 'runner')
|
||||
|
||||
|
@ -57,7 +57,7 @@ async function getMaxQualityVideoFile (req: express.Request, res: express.Respon
|
|||
|
||||
const file = video.getMaxQualityFile()
|
||||
|
||||
if (file.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
if (file.storage === FileStorage.OBJECT_STORAGE) {
|
||||
if (file.isHLS()) {
|
||||
return proxifyHLS({
|
||||
req,
|
||||
|
|
|
@ -151,7 +151,7 @@ async function searchVideoURI (url: string, res: express.Response) {
|
|||
logger.info('Cannot search remote video %s.', url, { err })
|
||||
}
|
||||
} else {
|
||||
video = await searchLocalUrl(sanitizeLocalUrl(url), url => VideoModel.loadByUrlAndPopulateAccount(url))
|
||||
video = await searchLocalUrl(sanitizeLocalUrl(url), url => VideoModel.loadByUrlAndPopulateAccountAndFiles(url))
|
||||
}
|
||||
|
||||
return res.json({
|
||||
|
|
|
@ -7,6 +7,7 @@ import { VideoChannelSyncLatestScheduler } from '@server/lib/schedulers/video-ch
|
|||
import { VideoViewsBufferScheduler } from '@server/lib/schedulers/video-views-buffer-scheduler.js'
|
||||
import { VideoViewsManager } from '@server/lib/views/video-views-manager.js'
|
||||
import { authenticate, ensureUserHasRight } from '../../../middlewares/index.js'
|
||||
import { RemoveExpiredUserExportsScheduler } from '@server/lib/schedulers/remove-expired-user-exports-scheduler.js'
|
||||
|
||||
const debugRouter = express.Router()
|
||||
|
||||
|
@ -42,6 +43,7 @@ async function runCommand (req: express.Request, res: express.Response) {
|
|||
|
||||
const processors: { [id in SendDebugCommand['command']]: () => Promise<any> } = {
|
||||
'remove-dandling-resumable-uploads': () => RemoveDanglingResumableUploadsScheduler.Instance.execute(),
|
||||
'remove-expired-user-exports': () => RemoveExpiredUserExportsScheduler.Instance.execute(),
|
||||
'process-video-views-buffer': () => VideoViewsBufferScheduler.Instance.execute(),
|
||||
'process-video-viewers': () => VideoViewsManager.Instance.processViewerStats(),
|
||||
'process-update-videos-scheduler': () => UpdateVideosScheduler.Instance.execute(),
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import 'multer'
|
||||
import express from 'express'
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { UserNotificationModel } from '@server/models/user/user-notification.js'
|
||||
import { getFormattedObjects } from '../../../helpers/utils.js'
|
||||
import {
|
||||
addAccountInBlocklist,
|
||||
|
@ -105,15 +103,9 @@ async function blockAccount (req: express.Request, res: express.Response) {
|
|||
const serverActor = await getServerActor()
|
||||
const accountToBlock = res.locals.account
|
||||
|
||||
await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id)
|
||||
await addAccountInBlocklist({ byAccountId: serverActor.Account.id, targetAccountId: accountToBlock.id, removeNotificationOfUserId: null })
|
||||
|
||||
UserNotificationModel.removeNotificationsOf({
|
||||
id: accountToBlock.id,
|
||||
type: 'account',
|
||||
forUserId: null // For all users
|
||||
}).catch(err => logger.error('Cannot remove notifications after an account mute.', { err }))
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function unblockAccount (req: express.Request, res: express.Response) {
|
||||
|
@ -121,7 +113,7 @@ async function unblockAccount (req: express.Request, res: express.Response) {
|
|||
|
||||
await removeAccountFromBlocklist(accountBlock)
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function listBlockedServers (req: express.Request, res: express.Response) {
|
||||
|
@ -142,15 +134,13 @@ async function blockServer (req: express.Request, res: express.Response) {
|
|||
const serverActor = await getServerActor()
|
||||
const serverToBlock = res.locals.server
|
||||
|
||||
await addServerInBlocklist(serverActor.Account.id, serverToBlock.id)
|
||||
await addServerInBlocklist({
|
||||
byAccountId: serverActor.Account.id,
|
||||
targetServerId: serverToBlock.id,
|
||||
removeNotificationOfUserId: null
|
||||
})
|
||||
|
||||
UserNotificationModel.removeNotificationsOf({
|
||||
id: serverToBlock.id,
|
||||
type: 'server',
|
||||
forUserId: null // For all users
|
||||
}).catch(err => logger.error('Cannot remove notifications after a server mute.', { err }))
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function unblockServer (req: express.Request, res: express.Response) {
|
||||
|
@ -158,5 +148,5 @@ async function unblockServer (req: express.Request, res: express.Response) {
|
|||
|
||||
await removeServerFromBlocklist(serverBlock)
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
|
|
@ -47,6 +47,8 @@ import { mySubscriptionsRouter } from './my-subscriptions.js'
|
|||
import { myVideoPlaylistsRouter } from './my-video-playlists.js'
|
||||
import { registrationsRouter } from './registrations.js'
|
||||
import { twoFactorRouter } from './two-factor.js'
|
||||
import { userExportsRouter } from './user-exports.js'
|
||||
import { userImportRouter } from './user-imports.js'
|
||||
|
||||
const auditLogger = auditLoggerFactory('users')
|
||||
|
||||
|
@ -55,6 +57,8 @@ const usersRouter = express.Router()
|
|||
usersRouter.use(apiRateLimiter)
|
||||
|
||||
usersRouter.use('/', emailVerificationRouter)
|
||||
usersRouter.use('/', userExportsRouter)
|
||||
usersRouter.use('/', userImportRouter)
|
||||
usersRouter.use('/', registrationsRouter)
|
||||
usersRouter.use('/', twoFactorRouter)
|
||||
usersRouter.use('/', tokensRouter)
|
||||
|
|
|
@ -262,11 +262,12 @@ async function updateMyAvatar (req: express.Request, res: express.Response) {
|
|||
|
||||
const userAccount = await AccountModel.load(user.Account.id)
|
||||
|
||||
const avatars = await updateLocalActorImageFiles(
|
||||
userAccount,
|
||||
avatarPhysicalFile,
|
||||
ActorImageType.AVATAR
|
||||
)
|
||||
const avatars = await updateLocalActorImageFiles({
|
||||
accountOrChannel: userAccount,
|
||||
imagePhysicalFile: avatarPhysicalFile,
|
||||
type: ActorImageType.AVATAR,
|
||||
sendActorUpdate: true
|
||||
})
|
||||
|
||||
return res.json({
|
||||
avatars: avatars.map(avatar => avatar.toFormattedJSON())
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import 'multer'
|
||||
import express from 'express'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { UserNotificationModel } from '@server/models/user/user-notification.js'
|
||||
import { getFormattedObjects } from '../../../helpers/utils.js'
|
||||
import {
|
||||
addAccountInBlocklist,
|
||||
|
@ -97,15 +95,9 @@ async function blockAccount (req: express.Request, res: express.Response) {
|
|||
const user = res.locals.oauth.token.User
|
||||
const accountToBlock = res.locals.account
|
||||
|
||||
await addAccountInBlocklist(user.Account.id, accountToBlock.id)
|
||||
await addAccountInBlocklist({ byAccountId: user.Account.id, targetAccountId: accountToBlock.id, removeNotificationOfUserId: user.id })
|
||||
|
||||
UserNotificationModel.removeNotificationsOf({
|
||||
id: accountToBlock.id,
|
||||
type: 'account',
|
||||
forUserId: user.id
|
||||
}).catch(err => logger.error('Cannot remove notifications after an account mute.', { err }))
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function unblockAccount (req: express.Request, res: express.Response) {
|
||||
|
@ -134,15 +126,13 @@ async function blockServer (req: express.Request, res: express.Response) {
|
|||
const user = res.locals.oauth.token.User
|
||||
const serverToBlock = res.locals.server
|
||||
|
||||
await addServerInBlocklist(user.Account.id, serverToBlock.id)
|
||||
await addServerInBlocklist({
|
||||
byAccountId: user.Account.id,
|
||||
targetServerId: serverToBlock.id,
|
||||
removeNotificationOfUserId: user.id
|
||||
})
|
||||
|
||||
UserNotificationModel.removeNotificationsOf({
|
||||
id: serverToBlock.id,
|
||||
type: 'server',
|
||||
forUserId: user.id
|
||||
}).catch(err => logger.error('Cannot remove notifications after a server mute.', { err }))
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function unblockServer (req: express.Request, res: express.Response) {
|
||||
|
@ -150,5 +140,5 @@ async function unblockServer (req: express.Request, res: express.Response) {
|
|||
|
||||
await removeServerFromBlocklist(serverBlock)
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
listUserNotificationsValidator,
|
||||
markAsReadUserNotificationsValidator,
|
||||
updateNotificationSettingsValidator
|
||||
} from '../../../middlewares/validators/user-notifications.js'
|
||||
} from '../../../middlewares/validators/users/user-notifications.js'
|
||||
import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting.js'
|
||||
import { meRouter } from './me.js'
|
||||
|
||||
|
@ -59,12 +59,6 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
|
|||
const user = res.locals.oauth.token.User
|
||||
const body = req.body as UserNotificationSetting
|
||||
|
||||
const query = {
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
}
|
||||
|
||||
const values: UserNotificationSetting = {
|
||||
newVideoFromSubscription: body.newVideoFromSubscription,
|
||||
newCommentOnMyVideo: body.newCommentOnMyVideo,
|
||||
|
@ -85,9 +79,9 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
|
|||
myVideoStudioEditionFinished: body.myVideoStudioEditionFinished
|
||||
}
|
||||
|
||||
await UserNotificationSettingModel.update(values, query)
|
||||
await UserNotificationSettingModel.updateUserSettings(values, user.id)
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function listUserNotifications (req: express.Request, res: express.Response) {
|
||||
|
@ -103,7 +97,7 @@ async function markAsReadUserNotifications (req: express.Request, res: express.R
|
|||
|
||||
await UserNotificationModel.markAsRead(user.id, req.body.ids)
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function markAsReadAllUserNotifications (req: express.Request, res: express.Response) {
|
||||
|
@ -111,5 +105,5 @@ async function markAsReadAllUserNotifications (req: express.Request, res: expres
|
|||
|
||||
await UserNotificationModel.markAllAsRead(user.id)
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import express from 'express'
|
||||
import { FileStorage, HttpStatusCode, UserExportRequest, UserExportRequestResult, UserExportState } from '@peertube/peertube-models'
|
||||
import {
|
||||
apiRateLimiter,
|
||||
asyncMiddleware,
|
||||
authenticate,
|
||||
userExportDeleteValidator,
|
||||
userExportRequestValidator,
|
||||
userExportsListValidator
|
||||
} from '../../../middlewares/index.js'
|
||||
import { UserExportModel } from '@server/models/user/user-export.js'
|
||||
import { getFormattedObjects } from '@server/helpers/utils.js'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { JobQueue } from '@server/lib/job-queue/job-queue.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
|
||||
const userExportsRouter = express.Router()
|
||||
|
||||
userExportsRouter.use(apiRateLimiter)
|
||||
|
||||
userExportsRouter.post('/:userId/exports/request',
|
||||
authenticate,
|
||||
asyncMiddleware(userExportRequestValidator),
|
||||
asyncMiddleware(requestExport)
|
||||
)
|
||||
|
||||
userExportsRouter.get('/:userId/exports',
|
||||
authenticate,
|
||||
asyncMiddleware(userExportsListValidator),
|
||||
asyncMiddleware(listUserExports)
|
||||
)
|
||||
|
||||
userExportsRouter.delete('/:userId/exports/:id',
|
||||
authenticate,
|
||||
asyncMiddleware(userExportDeleteValidator),
|
||||
asyncMiddleware(deleteUserExport)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
userExportsRouter
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function requestExport (req: express.Request, res: express.Response) {
|
||||
const body = req.body as UserExportRequest
|
||||
|
||||
const exportModel = new UserExportModel({
|
||||
state: UserExportState.PENDING,
|
||||
withVideoFiles: body.withVideoFiles,
|
||||
|
||||
storage: CONFIG.OBJECT_STORAGE.ENABLED
|
||||
? FileStorage.OBJECT_STORAGE
|
||||
: FileStorage.FILE_SYSTEM,
|
||||
|
||||
userId: res.locals.user.id,
|
||||
createdAt: new Date()
|
||||
})
|
||||
exportModel.generateAndSetFilename()
|
||||
|
||||
await sequelizeTypescript.transaction(async transaction => {
|
||||
await exportModel.save({ transaction })
|
||||
})
|
||||
|
||||
await JobQueue.Instance.createJob({ type: 'create-user-export', payload: { userExportId: exportModel.id } })
|
||||
|
||||
return res.json({
|
||||
export: {
|
||||
id: exportModel.id
|
||||
}
|
||||
} as UserExportRequestResult)
|
||||
}
|
||||
|
||||
async function listUserExports (req: express.Request, res: express.Response) {
|
||||
const resultList = await UserExportModel.listForApi({
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
user: res.locals.user
|
||||
})
|
||||
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
}
|
||||
|
||||
async function deleteUserExport (req: express.Request, res: express.Response) {
|
||||
const userExport = res.locals.userExport
|
||||
|
||||
await sequelizeTypescript.transaction(async transaction => {
|
||||
await userExport.reload({ transaction })
|
||||
|
||||
if (!userExport.canBeSafelyRemoved()) {
|
||||
return res.sendStatus(HttpStatusCode.CONFLICT_409)
|
||||
}
|
||||
|
||||
await userExport.destroy({ transaction })
|
||||
})
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import express from 'express'
|
||||
import {
|
||||
apiRateLimiter,
|
||||
asyncMiddleware,
|
||||
authenticate
|
||||
} from '../../../middlewares/index.js'
|
||||
import { uploadx } from '@server/lib/uploadx.js'
|
||||
import {
|
||||
getLatestImportStatusValidator,
|
||||
userImportRequestResumableInitValidator,
|
||||
userImportRequestResumableValidator
|
||||
} from '@server/middlewares/validators/users/user-import.js'
|
||||
import { HttpStatusCode, UserImportState, UserImportUploadResult } from '@peertube/peertube-models'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { UserImportModel } from '@server/models/user/user-import.js'
|
||||
import { getFSUserImportFilePath } from '@server/lib/paths.js'
|
||||
import { move } from 'fs-extra/esm'
|
||||
import { JobQueue } from '@server/lib/job-queue/job-queue.js'
|
||||
import { saveInTransactionWithRetries } from '@server/helpers/database-utils.js'
|
||||
|
||||
const userImportRouter = express.Router()
|
||||
|
||||
userImportRouter.use(apiRateLimiter)
|
||||
|
||||
userImportRouter.post('/:userId/imports/import-resumable',
|
||||
authenticate,
|
||||
asyncMiddleware(userImportRequestResumableInitValidator),
|
||||
(req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end
|
||||
)
|
||||
|
||||
userImportRouter.delete('/:userId/imports/import-resumable',
|
||||
authenticate,
|
||||
(req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end
|
||||
)
|
||||
|
||||
userImportRouter.put('/:userId/imports/import-resumable',
|
||||
authenticate,
|
||||
uploadx.upload, // uploadx doesn't next() before the file upload completes
|
||||
asyncMiddleware(userImportRequestResumableValidator),
|
||||
asyncMiddleware(addUserImportResumable)
|
||||
)
|
||||
|
||||
userImportRouter.get('/:userId/imports/latest',
|
||||
authenticate,
|
||||
asyncMiddleware(getLatestImportStatusValidator),
|
||||
asyncMiddleware(getLatestImport)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
userImportRouter
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function addUserImportResumable (req: express.Request, res: express.Response) {
|
||||
const file = res.locals.importUserFileResumable
|
||||
const user = res.locals.user
|
||||
|
||||
// Move import
|
||||
const userImport = new UserImportModel({
|
||||
state: UserImportState.PENDING,
|
||||
userId: user.id,
|
||||
createdAt: new Date()
|
||||
})
|
||||
userImport.generateAndSetFilename()
|
||||
|
||||
await move(file.path, getFSUserImportFilePath(userImport))
|
||||
|
||||
await saveInTransactionWithRetries(userImport)
|
||||
|
||||
// Create job
|
||||
await JobQueue.Instance.createJob({ type: 'import-user-archive', payload: { userImportId: userImport.id } })
|
||||
|
||||
logger.info('User import request job created for user ' + user.username)
|
||||
|
||||
return res.json({
|
||||
userImport: {
|
||||
id: userImport.id
|
||||
}
|
||||
} as UserImportUploadResult)
|
||||
}
|
||||
|
||||
async function getLatestImport (req: express.Request, res: express.Response) {
|
||||
const userImport = await UserImportModel.loadLatestByUserId(res.locals.user.id)
|
||||
if (!userImport) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
return res.json(userImport.toFormattedJSON())
|
||||
}
|
|
@ -213,7 +213,12 @@ async function updateVideoChannelBanner (req: express.Request, res: express.Resp
|
|||
const videoChannel = res.locals.videoChannel
|
||||
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
|
||||
|
||||
const banners = await updateLocalActorImageFiles(videoChannel, bannerPhysicalFile, ActorImageType.BANNER)
|
||||
const banners = await updateLocalActorImageFiles({
|
||||
accountOrChannel: videoChannel,
|
||||
imagePhysicalFile: bannerPhysicalFile,
|
||||
type: ActorImageType.BANNER,
|
||||
sendActorUpdate: true
|
||||
})
|
||||
|
||||
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
|
||||
|
||||
|
@ -227,7 +232,13 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp
|
|||
const videoChannel = res.locals.videoChannel
|
||||
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
|
||||
|
||||
const avatars = await updateLocalActorImageFiles(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR)
|
||||
const avatars = await updateLocalActorImageFiles({
|
||||
accountOrChannel: videoChannel,
|
||||
imagePhysicalFile: avatarPhysicalFile,
|
||||
type: ActorImageType.AVATAR,
|
||||
sendActorUpdate: true
|
||||
})
|
||||
|
||||
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
|
||||
|
||||
return res.json({
|
||||
|
|
|
@ -192,7 +192,6 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
|
|||
const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) as MVideoPlaylistFull
|
||||
|
||||
if (thumbnailModel) {
|
||||
thumbnailModel.automaticallyGenerated = false
|
||||
await videoPlaylistCreated.setAndSaveThumbnail(thumbnailModel, t)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import express from 'express'
|
||||
import { HttpStatusCode, UserVideoRateUpdate } from '@peertube/peertube-models'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { VIDEO_RATE_TYPES } from '../../../initializers/constants.js'
|
||||
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||
import { getLocalRateUrl, sendVideoRateChange } from '../../../lib/activitypub/video-rates.js'
|
||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares/index.js'
|
||||
import { AccountModel } from '../../../models/account/account.js'
|
||||
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
|
||||
import { userRateVideo } from '@server/lib/rate.js'
|
||||
|
||||
const rateVideoRouter = express.Router()
|
||||
|
||||
|
@ -25,63 +21,16 @@ export {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function rateVideo (req: express.Request, res: express.Response) {
|
||||
const body: UserVideoRateUpdate = req.body
|
||||
const rateType = body.rating
|
||||
const videoInstance = res.locals.videoAll
|
||||
const userAccount = res.locals.oauth.token.User.Account
|
||||
const user = res.locals.oauth.token.User
|
||||
const video = res.locals.videoAll
|
||||
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
const sequelizeOptions = { transaction: t }
|
||||
|
||||
const accountInstance = await AccountModel.load(userAccount.id, t)
|
||||
const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t)
|
||||
|
||||
// Same rate, nothing do to
|
||||
if (rateType === 'none' && !previousRate || previousRate?.type === rateType) return
|
||||
|
||||
let likesToIncrement = 0
|
||||
let dislikesToIncrement = 0
|
||||
|
||||
if (rateType === VIDEO_RATE_TYPES.LIKE) likesToIncrement++
|
||||
else if (rateType === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++
|
||||
|
||||
// There was a previous rate, update it
|
||||
if (previousRate) {
|
||||
// We will remove the previous rate, so we will need to update the video count attribute
|
||||
if (previousRate.type === 'like') likesToIncrement--
|
||||
else if (previousRate.type === 'dislike') dislikesToIncrement--
|
||||
|
||||
if (rateType === 'none') { // Destroy previous rate
|
||||
await previousRate.destroy(sequelizeOptions)
|
||||
} else { // Update previous rate
|
||||
previousRate.type = rateType
|
||||
previousRate.url = getLocalRateUrl(rateType, userAccount.Actor, videoInstance)
|
||||
await previousRate.save(sequelizeOptions)
|
||||
}
|
||||
} else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate
|
||||
const query = {
|
||||
accountId: accountInstance.id,
|
||||
videoId: videoInstance.id,
|
||||
type: rateType,
|
||||
url: getLocalRateUrl(rateType, userAccount.Actor, videoInstance)
|
||||
}
|
||||
|
||||
await AccountVideoRateModel.create(query, sequelizeOptions)
|
||||
}
|
||||
|
||||
const incrementQuery = {
|
||||
likes: likesToIncrement,
|
||||
dislikes: dislikesToIncrement
|
||||
}
|
||||
|
||||
await videoInstance.increment(incrementQuery, sequelizeOptions)
|
||||
|
||||
await sendVideoRateChange(accountInstance, videoInstance, likesToIncrement, dislikesToIncrement, t)
|
||||
|
||||
logger.info('Account video rate for video %s of account %s updated.', videoInstance.name, accountInstance.name)
|
||||
await userRateVideo({
|
||||
account: user.Account,
|
||||
rateType: (req.body as UserVideoRateUpdate).rating,
|
||||
video
|
||||
})
|
||||
|
||||
return res.type('json')
|
||||
.status(HttpStatusCode.NO_CONTENT_204)
|
||||
.end()
|
||||
logger.info('Account video rate for video %s of account %s updated.', video.name, user.username)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-q
|
|||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail.js'
|
||||
import { uploadx } from '@server/lib/uploadx.js'
|
||||
import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video.js'
|
||||
import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
|
||||
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist.js'
|
||||
import { buildNewFile } from '@server/lib/video-file.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
|
|
|
@ -6,7 +6,7 @@ import { exists } from '@server/helpers/custom-validators/misc.js'
|
|||
import { changeVideoChannelShare } from '@server/lib/activitypub/share.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import { setVideoPrivacy } from '@server/lib/video-privacy.js'
|
||||
import { addVideoJobsAfterUpdate, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video.js'
|
||||
import { buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video.js'
|
||||
import { openapiOperationDoc } from '@server/middlewares/doc.js'
|
||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import { FilteredModelAttributes } from '@server/types/index.js'
|
||||
|
@ -23,6 +23,7 @@ import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosU
|
|||
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import { replaceChaptersFromDescriptionIfNeeded } from '@server/lib/video-chapters.js'
|
||||
import { addVideoJobsAfterUpdate } from '@server/lib/video-jobs.js'
|
||||
|
||||
const lTags = loggerTagsFactory('api', 'video')
|
||||
const auditLogger = auditLoggerFactory('videos')
|
||||
|
|
|
@ -3,14 +3,10 @@ import { move } from 'fs-extra/esm'
|
|||
import { basename } from 'path'
|
||||
import { getResumableUploadPath } from '@server/helpers/upload.js'
|
||||
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url.js'
|
||||
import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue/index.js'
|
||||
import { Redis } from '@server/lib/redis.js'
|
||||
import { uploadx } from '@server/lib/uploadx.js'
|
||||
import {
|
||||
buildLocalVideoFromReq,
|
||||
buildMoveJob,
|
||||
buildStoryboardJobIfNeeded,
|
||||
buildVideoThumbnailsFromReq,
|
||||
buildLocalVideoFromReq, buildVideoThumbnailsFromReq,
|
||||
setVideoTags
|
||||
} from '@server/lib/video.js'
|
||||
import { buildNewFile } from '@server/lib/video-file.js'
|
||||
|
@ -21,7 +17,7 @@ import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
|||
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
||||
import { MVideoFile, MVideoFullLight, MVideoThumbnail } from '@server/types/models/index.js'
|
||||
import { uuidToShort } from '@peertube/peertube-node-utils'
|
||||
import { HttpStatusCode, ThumbnailType, VideoCreate, VideoPrivacy, VideoState } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, ThumbnailType, VideoCreate, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js'
|
||||
import { createReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||
|
@ -43,6 +39,7 @@ import { VideoModel } from '../../../models/video/video.js'
|
|||
import { ffprobePromise, getChaptersFromContainer } from '@peertube/peertube-ffmpeg'
|
||||
import { replaceChapters, replaceChaptersFromDescriptionIfNeeded } from '@server/lib/video-chapters.js'
|
||||
import { FfprobeData } from 'fluent-ffmpeg'
|
||||
import { addVideoJobsAfterCreation } from '@server/lib/video-jobs.js'
|
||||
|
||||
const lTags = loggerTagsFactory('api', 'video')
|
||||
const auditLogger = auditLoggerFactory('videos')
|
||||
|
@ -230,7 +227,7 @@ async function addVideo (options: {
|
|||
// Channel has a new content, set as updated
|
||||
await videoCreated.VideoChannel.setAsUpdated()
|
||||
|
||||
addVideoJobsAfterUpload(videoCreated, videoFile)
|
||||
addVideoJobsAfterCreation({ video: videoCreated, videoFile })
|
||||
.catch(err => logger.error('Cannot build new video jobs of %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) }))
|
||||
|
||||
Hooks.runAction('action:api.video.uploaded', { video: videoCreated, req, res })
|
||||
|
@ -244,55 +241,6 @@ async function addVideo (options: {
|
|||
}
|
||||
}
|
||||
|
||||
async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile) {
|
||||
const jobs: (CreateJobArgument & CreateJobOptions)[] = [
|
||||
{
|
||||
type: 'manage-video-torrent' as 'manage-video-torrent',
|
||||
payload: {
|
||||
videoId: video.id,
|
||||
videoFileId: videoFile.id,
|
||||
action: 'create'
|
||||
}
|
||||
},
|
||||
|
||||
buildStoryboardJobIfNeeded({ video, federate: false }),
|
||||
|
||||
{
|
||||
type: 'notify',
|
||||
payload: {
|
||||
action: 'new-video',
|
||||
videoUUID: video.uuid
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
type: 'federate-video' as 'federate-video',
|
||||
payload: {
|
||||
videoUUID: video.uuid,
|
||||
isNewVideoForFederation: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
|
||||
jobs.push(await buildMoveJob({ video, previousVideoState: undefined, type: 'move-to-object-storage' }))
|
||||
}
|
||||
|
||||
if (video.state === VideoState.TO_TRANSCODE) {
|
||||
jobs.push({
|
||||
type: 'transcoding-job-builder' as 'transcoding-job-builder',
|
||||
payload: {
|
||||
videoUUID: video.uuid,
|
||||
optimizeJob: {
|
||||
isNewVideo: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return JobQueue.Instance.createSequentialJobFlow(...jobs)
|
||||
}
|
||||
|
||||
async function deleteUploadResumableCache (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
await Redis.Instance.deleteUploadSession(req.query.upload_id)
|
||||
|
||||
|
|
|
@ -2,14 +2,30 @@ import cors from 'cors'
|
|||
import express from 'express'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { VideoTorrentsSimpleFileCache } from '@server/lib/files-cache/index.js'
|
||||
import { generateHLSFilePresignedUrl, generateWebVideoPresignedUrl } from '@server/lib/object-storage/index.js'
|
||||
import {
|
||||
generateHLSFilePresignedUrl,
|
||||
generateUserExportPresignedUrl,
|
||||
generateWebVideoPresignedUrl
|
||||
} from '@server/lib/object-storage/index.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||
import {
|
||||
MStreamingPlaylist,
|
||||
MStreamingPlaylistVideo,
|
||||
MUserExport,
|
||||
MVideo,
|
||||
MVideoFile,
|
||||
MVideoFullLight
|
||||
} from '@server/types/models/index.js'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, VideoStorage, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, FileStorage, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||
import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants.js'
|
||||
import { asyncMiddleware, optionalAuthenticate, videosDownloadValidator } from '../middlewares/index.js'
|
||||
import {
|
||||
asyncMiddleware, optionalAuthenticate,
|
||||
userExportDownloadValidator,
|
||||
videosDownloadValidator
|
||||
} from '../middlewares/index.js'
|
||||
import { getFSUserExportFilePath } from '@server/lib/paths.js'
|
||||
|
||||
const downloadRouter = express.Router()
|
||||
|
||||
|
@ -34,6 +50,12 @@ downloadRouter.use(
|
|||
asyncMiddleware(downloadHLSVideoFile)
|
||||
)
|
||||
|
||||
downloadRouter.use(
|
||||
STATIC_DOWNLOAD_PATHS.USER_EXPORT + ':filename',
|
||||
asyncMiddleware(userExportDownloadValidator), // Include JWT token authentication
|
||||
asyncMiddleware(downloadUserExport)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
|
@ -99,8 +121,8 @@ async function downloadVideoFile (req: express.Request, res: express.Response) {
|
|||
const videoName = video.name.replace(/[/\\]/g, '_')
|
||||
const downloadFilename = `${videoName}-${videoFile.resolution}p${videoFile.extname}`
|
||||
|
||||
if (videoFile.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
return redirectToObjectStorage({ req, res, video, file: videoFile, downloadFilename })
|
||||
if (videoFile.storage === FileStorage.OBJECT_STORAGE) {
|
||||
return redirectVideoDownloadToObjectStorage({ res, video, file: videoFile, downloadFilename })
|
||||
}
|
||||
|
||||
await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), path => {
|
||||
|
@ -140,8 +162,8 @@ async function downloadHLSVideoFile (req: express.Request, res: express.Response
|
|||
const videoName = video.name.replace(/\//g, '_')
|
||||
const downloadFilename = `${videoName}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}`
|
||||
|
||||
if (videoFile.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
return redirectToObjectStorage({ req, res, video, streamingPlaylist, file: videoFile, downloadFilename })
|
||||
if (videoFile.storage === FileStorage.OBJECT_STORAGE) {
|
||||
return redirectVideoDownloadToObjectStorage({ res, video, streamingPlaylist, file: videoFile, downloadFilename })
|
||||
}
|
||||
|
||||
await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(streamingPlaylist), path => {
|
||||
|
@ -149,6 +171,21 @@ async function downloadHLSVideoFile (req: express.Request, res: express.Response
|
|||
})
|
||||
}
|
||||
|
||||
function downloadUserExport (req: express.Request, res: express.Response) {
|
||||
const userExport = res.locals.userExport
|
||||
|
||||
const downloadFilename = userExport.filename
|
||||
|
||||
if (userExport.storage === FileStorage.OBJECT_STORAGE) {
|
||||
return redirectUserExportToObjectStorage({ res, userExport, downloadFilename })
|
||||
}
|
||||
|
||||
res.download(getFSUserExportFilePath(userExport), downloadFilename)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getVideoFile (req: express.Request, files: MVideoFile[]) {
|
||||
const resolution = forceNumber(req.params.resolution)
|
||||
return files.find(f => f.resolution === resolution)
|
||||
|
@ -194,8 +231,7 @@ function checkAllowResult (res: express.Response, allowParameters: any, result?:
|
|||
return true
|
||||
}
|
||||
|
||||
async function redirectToObjectStorage (options: {
|
||||
req: express.Request
|
||||
async function redirectVideoDownloadToObjectStorage (options: {
|
||||
res: express.Response
|
||||
video: MVideo
|
||||
file: MVideoFile
|
||||
|
@ -212,3 +248,17 @@ async function redirectToObjectStorage (options: {
|
|||
|
||||
return res.redirect(url)
|
||||
}
|
||||
|
||||
async function redirectUserExportToObjectStorage (options: {
|
||||
res: express.Response
|
||||
downloadFilename: string
|
||||
userExport: MUserExport
|
||||
}) {
|
||||
const { res, downloadFilename, userExport } = options
|
||||
|
||||
const url = await generateUserExportPresignedUrl({ userExport, downloadFilename })
|
||||
|
||||
logger.debug('Generating pre-signed URL %s for user export %s', url, userExport.filename)
|
||||
|
||||
return res.redirect(url)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import { createReadStream, createWriteStream } from 'fs'
|
||||
import { move, remove } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { Transform } from 'stream'
|
||||
import { MVideoCaption } from '@server/types/models/index.js'
|
||||
import { CONFIG } from '../initializers/config.js'
|
||||
import { pipelinePromise } from './core-utils.js'
|
||||
|
||||
async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: MVideoCaption) {
|
||||
const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR
|
||||
const destination = join(videoCaptionsDir, videoCaption.filename)
|
||||
async function moveAndProcessCaptionFile (physicalFile: { filename?: string, path: string }, videoCaption: MVideoCaption) {
|
||||
const destination = videoCaption.getFSPath()
|
||||
|
||||
// Convert this srt file to vtt
|
||||
if (physicalFile.path.endsWith('.srt')) {
|
||||
|
@ -19,7 +16,7 @@ async function moveAndProcessCaptionFile (physicalFile: { filename: string, path
|
|||
}
|
||||
|
||||
// This is important in case if there is another attempt in the retry process
|
||||
physicalFile.filename = videoCaption.filename
|
||||
if (physicalFile.filename) physicalFile.filename = videoCaption.filename
|
||||
physicalFile.path = destination
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import { createWriteStream } from 'fs'
|
||||
import { ensureDir } from 'fs-extra/esm'
|
||||
import { dirname, join } from 'path'
|
||||
import { pipeline } from 'stream'
|
||||
import * as yauzl from 'yauzl'
|
||||
import { logger, loggerTagsFactory } from './logger.js'
|
||||
|
||||
const lTags = loggerTagsFactory('unzip')
|
||||
|
||||
export async function unzip (source: string, destination: string) {
|
||||
await ensureDir(destination)
|
||||
|
||||
logger.info(`Unzip ${source} to ${destination}`, lTags())
|
||||
|
||||
return new Promise<void>((res, rej) => {
|
||||
yauzl.open(source, { lazyEntries: true }, (err, zipFile) => {
|
||||
if (err) return rej(err)
|
||||
|
||||
zipFile.readEntry()
|
||||
|
||||
zipFile.on('entry', async entry => {
|
||||
const entryPath = join(destination, entry.fileName)
|
||||
|
||||
try {
|
||||
if (/\/$/.test(entry.fileName)) {
|
||||
await ensureDir(entryPath)
|
||||
logger.debug(`Creating directory from zip ${entryPath}`, lTags())
|
||||
|
||||
zipFile.readEntry()
|
||||
return
|
||||
}
|
||||
|
||||
await ensureDir(dirname(entryPath))
|
||||
} catch (err) {
|
||||
return rej(err)
|
||||
}
|
||||
|
||||
zipFile.openReadStream(entry, (readErr, readStream) => {
|
||||
if (readErr) return rej(readErr)
|
||||
|
||||
logger.debug(`Creating file from zip ${entryPath}`, lTags())
|
||||
|
||||
const writeStream = createWriteStream(entryPath)
|
||||
writeStream.on('close', () => zipFile.readEntry())
|
||||
|
||||
pipeline(readStream, writeStream, pipelineErr => {
|
||||
if (pipelineErr) return rej(pipelineErr)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
zipFile.on('end', () => res())
|
||||
})
|
||||
})
|
||||
}
|
|
@ -153,6 +153,11 @@ const CONFIG = {
|
|||
BUCKET_NAME: config.get<string>('object_storage.streaming_playlists.bucket_name'),
|
||||
PREFIX: config.get<string>('object_storage.streaming_playlists.prefix'),
|
||||
BASE_URL: config.get<string>('object_storage.streaming_playlists.base_url')
|
||||
},
|
||||
USER_EXPORTS: {
|
||||
BUCKET_NAME: config.get<string>('object_storage.user_exports.bucket_name'),
|
||||
PREFIX: config.get<string>('object_storage.user_exports.prefix'),
|
||||
BASE_URL: config.get<string>('object_storage.user_exports.base_url')
|
||||
}
|
||||
},
|
||||
WEBSERVER: {
|
||||
|
@ -511,6 +516,16 @@ const CONFIG = {
|
|||
get FULL_SYNC_VIDEOS_LIMIT () {
|
||||
return config.get<number>('import.video_channel_synchronization.full_sync_videos_limit')
|
||||
}
|
||||
},
|
||||
USERS: {
|
||||
get ENABLED () { return config.get<boolean>('import.users.enabled') }
|
||||
}
|
||||
},
|
||||
EXPORT: {
|
||||
USERS: {
|
||||
get ENABLED () { return config.get<boolean>('export.users.enabled') },
|
||||
get MAX_USER_VIDEO_QUOTA () { return parseBytes(config.get<string>('export.users.max_user_video_quota')) },
|
||||
get EXPORT_EXPIRATION () { return parseDurationToMs(config.get<string>('export.users.export_expiration')) }
|
||||
}
|
||||
},
|
||||
AUTO_BLACKLIST: {
|
||||
|
|
|
@ -10,6 +10,10 @@ import {
|
|||
NSFWPolicyType,
|
||||
RunnerJobState,
|
||||
RunnerJobStateType,
|
||||
UserExportState,
|
||||
UserExportStateType,
|
||||
UserImportState,
|
||||
UserImportStateType,
|
||||
UserRegistrationState,
|
||||
UserRegistrationStateType,
|
||||
VideoChannelSyncState,
|
||||
|
@ -41,7 +45,7 @@ import { cpus } from 'os'
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 805
|
||||
const LAST_MIGRATION_VERSION = 815
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -191,7 +195,9 @@ const JOB_ATTEMPTS: { [id in JobType]: number } = {
|
|||
'transcoding-job-builder': 1,
|
||||
'generate-video-storyboard': 1,
|
||||
'notify': 1,
|
||||
'federate-video': 1
|
||||
'federate-video': 1,
|
||||
'create-user-export': 1,
|
||||
'import-user-archive': 1
|
||||
}
|
||||
// Excluded keys are jobs that can be configured by admins
|
||||
const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-import'>]: number } = {
|
||||
|
@ -217,7 +223,9 @@ const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-im
|
|||
'transcoding-job-builder': 1,
|
||||
'generate-video-storyboard': 1,
|
||||
'notify': 5,
|
||||
'federate-video': 3
|
||||
'federate-video': 3,
|
||||
'create-user-export': 1,
|
||||
'import-user-archive': 1
|
||||
}
|
||||
const JOB_TTL: { [id in JobType]: number } = {
|
||||
'activitypub-http-broadcast': 60000 * 10, // 10 minutes
|
||||
|
@ -244,7 +252,9 @@ const JOB_TTL: { [id in JobType]: number } = {
|
|||
'after-video-channel-import': 60000 * 5, // 5 minutes
|
||||
'transcoding-job-builder': 60000, // 1 minute
|
||||
'notify': 60000 * 5, // 5 minutes
|
||||
'federate-video': 60000 * 5 // 5 minutes
|
||||
'federate-video': 60000 * 5, // 5 minutes,
|
||||
'create-user-export': 60000 * 60 * 24, // 24 hours
|
||||
'import-user-archive': 60000 * 60 * 24 // 24 hours
|
||||
}
|
||||
const REPEAT_JOBS: { [ id in JobType ]?: RepeatOptions } = {
|
||||
'videos-views-stats': {
|
||||
|
@ -313,6 +323,7 @@ const SCHEDULER_INTERVALS_MS = {
|
|||
AUTO_FOLLOW_INDEX_INSTANCES: 60000 * 60 * 24, // 1 day
|
||||
REMOVE_OLD_VIEWS: 60000 * 60 * 24, // 1 day
|
||||
REMOVE_OLD_HISTORY: 60000 * 60 * 24, // 1 day
|
||||
REMOVE_EXPIRED_USER_EXPORTS: 1000 * 3600, // 1 hour
|
||||
UPDATE_INBOX_STATS: 1000 * 60, // 1 minute
|
||||
REMOVE_DANGLING_RESUMABLE_UPLOADS: 60000 * 60, // 1 hour
|
||||
CHANNEL_SYNC_CHECK_INTERVAL: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.CHECK_INTERVAL
|
||||
|
@ -503,6 +514,10 @@ const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {
|
|||
DISLIKE: 'dislike'
|
||||
}
|
||||
|
||||
const USER_IMPORT = {
|
||||
MAX_PLAYLIST_ELEMENTS: 1000
|
||||
}
|
||||
|
||||
const FFMPEG_NICE = {
|
||||
// parent process defaults to niceness = 0
|
||||
// reminder: lower = higher priority, max value is 19, lowest is -20
|
||||
|
@ -618,6 +633,20 @@ const RUNNER_JOB_STATES: { [ id in RunnerJobStateType ]: string } = {
|
|||
[RunnerJobState.PARENT_CANCELLED]: 'Parent job cancelled'
|
||||
}
|
||||
|
||||
const USER_EXPORT_STATES: { [ id in UserExportStateType ]: string } = {
|
||||
[UserExportState.PENDING]: 'Pending',
|
||||
[UserExportState.PROCESSING]: 'Processing',
|
||||
[UserExportState.COMPLETED]: 'Completed',
|
||||
[UserExportState.ERRORED]: 'Failed'
|
||||
}
|
||||
|
||||
const USER_IMPORT_STATES: { [ id in UserImportStateType ]: string } = {
|
||||
[UserImportState.PENDING]: 'Pending',
|
||||
[UserImportState.PROCESSING]: 'Processing',
|
||||
[UserImportState.COMPLETED]: 'Completed',
|
||||
[UserImportState.ERRORED]: 'Failed'
|
||||
}
|
||||
|
||||
const MIMETYPES = {
|
||||
AUDIO: {
|
||||
MIMETYPE_EXT: {
|
||||
|
@ -773,6 +802,7 @@ const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes
|
|||
const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
|
||||
|
||||
const TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME = 60000 * 10 // 10 minutes
|
||||
let JWT_TOKEN_USER_EXPORT_FILE_LIFETIME = '15 minutes'
|
||||
|
||||
const EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
|
||||
|
||||
|
@ -807,7 +837,8 @@ const STATIC_PATHS = {
|
|||
const STATIC_DOWNLOAD_PATHS = {
|
||||
TORRENTS: '/download/torrents/',
|
||||
VIDEOS: '/download/videos/',
|
||||
HLS_VIDEOS: '/download/streaming-playlists/hls/videos/'
|
||||
HLS_VIDEOS: '/download/streaming-playlists/hls/videos/',
|
||||
USER_EXPORT: '/download/user-export/'
|
||||
}
|
||||
const LAZY_STATIC_PATHS = {
|
||||
THUMBNAILS: '/lazy-static/thumbnails/',
|
||||
|
@ -1125,6 +1156,8 @@ if (process.env.PRODUCTION_CONSTANTS !== 'true') {
|
|||
VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION = 1
|
||||
|
||||
RUNNER_JOBS.LAST_CONTACT_UPDATE_INTERVAL = 2000
|
||||
|
||||
JWT_TOKEN_USER_EXPORT_FILE_LIFETIME = '2 seconds'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1168,6 +1201,8 @@ export {
|
|||
DIRECTORIES,
|
||||
RESUMABLE_UPLOAD_SESSION_LIFETIME,
|
||||
RUNNER_JOB_STATES,
|
||||
USER_EXPORT_STATES,
|
||||
USER_IMPORT_STATES,
|
||||
P2P_MEDIA_LOADER_PEER_VERSION,
|
||||
STORYBOARD,
|
||||
ACTOR_IMAGES_SIZE,
|
||||
|
@ -1187,6 +1222,7 @@ export {
|
|||
STATS_TIMESERIE,
|
||||
BROADCAST_CONCURRENCY,
|
||||
AUDIT_LOG_FILENAME,
|
||||
USER_IMPORT,
|
||||
PAGINATION,
|
||||
ACTOR_FOLLOW_SCORE,
|
||||
PREVIEWS_SIZE,
|
||||
|
@ -1195,6 +1231,7 @@ export {
|
|||
DEFAULT_USER_THEME_NAME,
|
||||
SERVER_ACTOR_NAME,
|
||||
TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME,
|
||||
JWT_TOKEN_USER_EXPORT_FILE_LIFETIME,
|
||||
PLUGIN_GLOBAL_CSS_FILE_NAME,
|
||||
PLUGIN_GLOBAL_CSS_PATH,
|
||||
PRIVATE_RSA_KEY_SIZE,
|
||||
|
|
|
@ -60,6 +60,8 @@ import { VideoModel } from '../models/video/video.js'
|
|||
import { VideoViewModel } from '../models/view/video-view.js'
|
||||
import { CONFIG } from './config.js'
|
||||
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
||||
import { UserExportModel } from '@server/models/user/user-export.js'
|
||||
import { UserImportModel } from '@server/models/user/user-import.js'
|
||||
|
||||
pg.defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||
|
||||
|
@ -165,6 +167,7 @@ async function initDatabaseModels (silent: boolean) {
|
|||
VideoTrackerModel,
|
||||
PluginModel,
|
||||
ActorCustomPageModel,
|
||||
UserImportModel,
|
||||
VideoJobInfoModel,
|
||||
VideoChannelSyncModel,
|
||||
UserRegistrationModel,
|
||||
|
@ -172,7 +175,8 @@ async function initDatabaseModels (silent: boolean) {
|
|||
RunnerRegistrationTokenModel,
|
||||
RunnerModel,
|
||||
RunnerJobModel,
|
||||
StoryboardModel
|
||||
StoryboardModel,
|
||||
UserExportModel
|
||||
])
|
||||
|
||||
// Check extensions exist in the database
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as Sequelize from 'sequelize'
|
||||
import { VideoStorage } from '@peertube/peertube-models'
|
||||
import { FileStorage } from '@peertube/peertube-models'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
|
@ -27,7 +27,7 @@ async function up (utils: {
|
|||
await utils.queryInterface.addColumn('videoFile', 'storage', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: VideoStorage.FILE_SYSTEM
|
||||
defaultValue: FileStorage.FILE_SYSTEM
|
||||
})
|
||||
await utils.queryInterface.changeColumn('videoFile', 'storage', { type: Sequelize.INTEGER, allowNull: false, defaultValue: null })
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ async function up (utils: {
|
|||
await utils.queryInterface.addColumn('videoStreamingPlaylist', 'storage', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: VideoStorage.FILE_SYSTEM
|
||||
defaultValue: FileStorage.FILE_SYSTEM
|
||||
})
|
||||
await utils.queryInterface.changeColumn('videoStreamingPlaylist', 'storage', {
|
||||
type: Sequelize.INTEGER,
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
}): Promise<void> {
|
||||
const query = `
|
||||
CREATE TABLE IF NOT EXISTS "userExport" (
|
||||
"id" SERIAL,
|
||||
"filename" VARCHAR(255),
|
||||
"withVideoFiles" BOOLEAN NOT NULL,
|
||||
"state" INTEGER NOT NULL,
|
||||
"error" TEXT,
|
||||
"size" INTEGER,
|
||||
"storage" INTEGER NOT NULL,
|
||||
"userId" INTEGER NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);`
|
||||
|
||||
await utils.sequelize.query(query, { transaction: utils.transaction })
|
||||
}
|
||||
|
||||
function down (options) {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
}): Promise<void> {
|
||||
const query = `
|
||||
CREATE TABLE IF NOT EXISTS "userImport" (
|
||||
"id" SERIAL,
|
||||
"filename" VARCHAR(255),
|
||||
"state" INTEGER NOT NULL,
|
||||
"error" TEXT,
|
||||
"resultSummary" JSONB,
|
||||
"userId" INTEGER NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);;`
|
||||
|
||||
await utils.sequelize.query(query, { transaction: utils.transaction })
|
||||
}
|
||||
|
||||
function down (options) {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
|
@ -7,7 +7,7 @@ import { forceNumber } from '@peertube/peertube-core-utils'
|
|||
|
||||
type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
|
||||
|
||||
async function activityPubCollectionPagination (
|
||||
export async function activityPubCollectionPagination (
|
||||
baseUrl: string,
|
||||
handler: ActivityPubCollectionPaginationHandler,
|
||||
page?: any,
|
||||
|
@ -56,8 +56,11 @@ async function activityPubCollectionPagination (
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
activityPubCollectionPagination
|
||||
export function activityPubCollection <T> (baseUrl: string, items: T[]) {
|
||||
return {
|
||||
id: baseUrl,
|
||||
type: 'OrderedCollection' as 'OrderedCollection',
|
||||
totalItems: items.length,
|
||||
orderedItems: items
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
|
|||
}
|
||||
|
||||
{
|
||||
const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(objectUrl)
|
||||
const videoInstance = await VideoModel.loadByUrlAndPopulateAccountAndFiles(objectUrl)
|
||||
if (videoInstance) {
|
||||
if (videoInstance.isOwned()) throw new Error(`Remote instance cannot delete owned video ${videoInstance.url}.`)
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ async function processCreateAbuse (flag: ActivityFlag, byActor: MActorSignature)
|
|||
logger.debug('Reporting remote abuse for object %s.', uri)
|
||||
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
const video = await VideoModel.loadByUrlAndPopulateAccount(uri, t)
|
||||
const video = await VideoModel.loadByUrlAndPopulateAccountAndFiles(uri, t)
|
||||
let videoComment: MCommentOwnerVideo
|
||||
let flaggedAccount: MAccountDefault
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ async function refreshVideoIfNeeded (options: {
|
|||
// We need more attributes if the argument video was fetched with not enough joints
|
||||
const video = options.fetchedType === 'all'
|
||||
? options.video as MVideoAccountLightBlacklistAllFiles
|
||||
: await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
|
||||
: await VideoModel.loadByUrlAndPopulateAccountAndFiles(options.video.url)
|
||||
|
||||
const lTags = loggerTagsFactory('ap', 'video', 'refresh', video.uuid, video.url)
|
||||
|
||||
|
|
|
@ -3,23 +3,51 @@ import { getServerActor } from '@server/models/application/application.js'
|
|||
import { MAccountBlocklist, MAccountId, MAccountHost, MServerBlocklist } from '@server/types/models/index.js'
|
||||
import { AccountBlocklistModel } from '../models/account/account-blocklist.js'
|
||||
import { ServerBlocklistModel } from '../models/server/server-blocklist.js'
|
||||
import { UserNotificationModel } from '@server/models/user/user-notification.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
|
||||
function addAccountInBlocklist (byAccountId: number, targetAccountId: number) {
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
async function addAccountInBlocklist (options: {
|
||||
byAccountId: number
|
||||
targetAccountId: number
|
||||
|
||||
removeNotificationOfUserId: number | null // If blocked by a user
|
||||
}) {
|
||||
const { byAccountId, targetAccountId, removeNotificationOfUserId } = options
|
||||
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
return AccountBlocklistModel.upsert({
|
||||
accountId: byAccountId,
|
||||
targetAccountId
|
||||
}, { transaction: t })
|
||||
})
|
||||
|
||||
UserNotificationModel.removeNotificationsOf({
|
||||
id: targetAccountId,
|
||||
type: 'account',
|
||||
forUserId: removeNotificationOfUserId
|
||||
}).catch(err => logger.error('Cannot remove notifications after an account mute.', { err }))
|
||||
}
|
||||
|
||||
function addServerInBlocklist (byAccountId: number, targetServerId: number) {
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
async function addServerInBlocklist (options: {
|
||||
byAccountId: number
|
||||
targetServerId: number
|
||||
|
||||
removeNotificationOfUserId: number | null
|
||||
}) {
|
||||
const { byAccountId, targetServerId, removeNotificationOfUserId } = options
|
||||
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
return ServerBlocklistModel.upsert({
|
||||
accountId: byAccountId,
|
||||
targetServerId
|
||||
}, { transaction: t })
|
||||
})
|
||||
|
||||
UserNotificationModel.removeNotificationsOf({
|
||||
id: targetServerId,
|
||||
type: 'server',
|
||||
forUserId: removeNotificationOfUserId
|
||||
}).catch(err => logger.error('Cannot remove notifications after a server mute.', { err }))
|
||||
}
|
||||
|
||||
function removeAccountFromBlocklist (accountBlock: MAccountBlocklist) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { EmailPayload, SendEmailDefaultOptions, UserRegistrationState } from '@peertube/peertube-models'
|
||||
import { EmailPayload, SendEmailDefaultOptions, UserExportState, UserRegistrationState } from '@peertube/peertube-models'
|
||||
import { isTestOrDevInstance, root } from '@peertube/peertube-node-utils'
|
||||
import { readFileSync } from 'fs'
|
||||
import merge from 'lodash-es/merge.js'
|
||||
|
@ -8,8 +8,9 @@ import { join } from 'path'
|
|||
import { bunyanLogger, logger } from '../helpers/logger.js'
|
||||
import { CONFIG, isEmailEnabled } from '../initializers/config.js'
|
||||
import { WEBSERVER } from '../initializers/constants.js'
|
||||
import { MRegistration, MUser } from '../types/models/index.js'
|
||||
import { MRegistration, MUser, MUserExport, MUserImport } from '../types/models/index.js'
|
||||
import { JobQueue } from './job-queue/index.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
|
||||
class Emailer {
|
||||
|
||||
|
@ -52,6 +53,8 @@ class Emailer {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
|
||||
const emailPayload: EmailPayload = {
|
||||
template: 'password-reset',
|
||||
|
@ -160,13 +163,82 @@ class Emailer {
|
|||
locals: {
|
||||
username: registration.username,
|
||||
moderationResponse: registration.moderationResponse,
|
||||
loginLink: WEBSERVER.URL + '/login'
|
||||
loginLink: WEBSERVER.URL + '/login',
|
||||
|
||||
hideNotificationPreferencesLink: true
|
||||
}
|
||||
}
|
||||
|
||||
return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async addUserExportCompletedOrErroredJob (userExport: MUserExport) {
|
||||
let template: string
|
||||
let subject: string
|
||||
|
||||
if (userExport.state === UserExportState.COMPLETED) {
|
||||
template = 'user-export-completed'
|
||||
subject = `Your export archive has been created`
|
||||
} else {
|
||||
template = 'user-export-errored'
|
||||
subject = `Failed to create your export archive`
|
||||
}
|
||||
|
||||
const user = await UserModel.loadById(userExport.userId)
|
||||
|
||||
const emailPayload: EmailPayload = {
|
||||
to: [ user.email ],
|
||||
template,
|
||||
subject,
|
||||
locals: {
|
||||
exportsUrl: WEBSERVER.URL + '/my-account/import-export',
|
||||
errorMessage: userExport.error,
|
||||
|
||||
hideNotificationPreferencesLink: true
|
||||
}
|
||||
}
|
||||
|
||||
return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
|
||||
}
|
||||
|
||||
async addUserImportErroredJob (userImport: MUserImport) {
|
||||
const user = await UserModel.loadById(userImport.userId)
|
||||
|
||||
const emailPayload: EmailPayload = {
|
||||
to: [ user.email ],
|
||||
template: 'user-import-errored',
|
||||
subject: 'Failed to import your archive',
|
||||
locals: {
|
||||
errorMessage: userImport.error,
|
||||
|
||||
hideNotificationPreferencesLink: true
|
||||
}
|
||||
}
|
||||
|
||||
return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
|
||||
}
|
||||
|
||||
async addUserImportSuccessJob (userImport: MUserImport) {
|
||||
const user = await UserModel.loadById(userImport.userId)
|
||||
|
||||
const emailPayload: EmailPayload = {
|
||||
to: [ user.email ],
|
||||
template: 'user-import-completed',
|
||||
subject: 'Your archive import has finished',
|
||||
locals: {
|
||||
resultStats: userImport.resultSummary.stats,
|
||||
|
||||
hideNotificationPreferencesLink: true
|
||||
}
|
||||
}
|
||||
|
||||
return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async sendMail (options: EmailPayload) {
|
||||
if (!isEmailEnabled()) {
|
||||
logger.info('Cannot send mail because SMTP is not configured.')
|
||||
|
@ -233,14 +305,14 @@ class Emailer {
|
|||
private initSMTPTransport () {
|
||||
logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
|
||||
|
||||
let tls
|
||||
let tls: { ca: [ Buffer ] }
|
||||
if (CONFIG.SMTP.CA_FILE) {
|
||||
tls = {
|
||||
ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ]
|
||||
}
|
||||
}
|
||||
|
||||
let auth
|
||||
let auth: { user: string, pass: string }
|
||||
if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) {
|
||||
auth = {
|
||||
user: CONFIG.SMTP.USERNAME,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
extends ../common/greetings
|
||||
include ../common/mixins.pug
|
||||
|
||||
block title
|
||||
| Your export archive has been created
|
||||
|
||||
block content
|
||||
p
|
||||
| Your export archive has been created. You can download it in #[a(href=exportsUrl) your account export page].
|
|
@ -0,0 +1,12 @@
|
|||
extends ../common/greetings
|
||||
include ../common/mixins.pug
|
||||
|
||||
block title
|
||||
| Failed to create your export archive
|
||||
|
||||
block content
|
||||
p
|
||||
| We are sorry but the generation of your export archive has failed:
|
||||
blockquote !{errorMessage}
|
||||
p
|
||||
| Please contact your administrator if the problem occurs again.
|
|
@ -0,0 +1,46 @@
|
|||
extends ../common/greetings
|
||||
include ../common/mixins.pug
|
||||
|
||||
mixin displaySummary(stats)
|
||||
ul
|
||||
if stats.success
|
||||
li Imported: #{stats.success}
|
||||
if stats.duplicates
|
||||
li Not imported as considered duplicate: #{stats.duplicates}
|
||||
if stats.errors
|
||||
li Not imported due to error: #{stats.errors}
|
||||
|
||||
block title
|
||||
| Your archive import has finished
|
||||
|
||||
block content
|
||||
p Your archive import has finished. Here is the summary of imported objects:
|
||||
|
||||
ul
|
||||
li
|
||||
strong User settings:
|
||||
+displaySummary(resultStats.userSettings)
|
||||
li
|
||||
strong Account (name, description, avatar...):
|
||||
+displaySummary(resultStats.account)
|
||||
li
|
||||
strong Blocklist:
|
||||
+displaySummary(resultStats.blocklist)
|
||||
li
|
||||
strong Channels:
|
||||
+displaySummary(resultStats.channels)
|
||||
li
|
||||
strong Likes:
|
||||
+displaySummary(resultStats.likes)
|
||||
li
|
||||
strong Dislikes:
|
||||
+displaySummary(resultStats.dislikes)
|
||||
li
|
||||
strong Subscriptions:
|
||||
+displaySummary(resultStats.following)
|
||||
li
|
||||
strong Video Playlists:
|
||||
+displaySummary(resultStats.videoPlaylists)
|
||||
li
|
||||
strong Videos:
|
||||
+displaySummary(resultStats.videos)
|
|
@ -0,0 +1,12 @@
|
|||
extends ../common/greetings
|
||||
include ../common/mixins.pug
|
||||
|
||||
block title
|
||||
| Failed to import your archive
|
||||
|
||||
block content
|
||||
p
|
||||
| We are sorry but the import of your archive has failed:
|
||||
blockquote !{errorMessage}
|
||||
p
|
||||
| Please contact your administrator if the problem occurs again.
|
|
@ -1,7 +1,6 @@
|
|||
import { join } from 'path'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { doRequestAndSaveToFile } from '@server/helpers/requests.js'
|
||||
import { CONFIG } from '../../initializers/config.js'
|
||||
import { FILES_CACHE } from '../../initializers/constants.js'
|
||||
import { VideoModel } from '../../models/video/video.js'
|
||||
import { VideoCaptionModel } from '../../models/video/video-caption.js'
|
||||
|
@ -24,7 +23,7 @@ class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
|||
if (!videoCaption) return undefined
|
||||
|
||||
if (videoCaption.isOwned()) {
|
||||
return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.filename) }
|
||||
return { isOwned: true, path: videoCaption.getFSPath() }
|
||||
}
|
||||
|
||||
return this.loadRemoteFile(filename)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { uniqify, uuidRegex } from '@peertube/peertube-core-utils'
|
||||
import { getVideoStreamDimensionsInfo } from '@peertube/peertube-ffmpeg'
|
||||
import { VideoStorage } from '@peertube/peertube-models'
|
||||
import { FileStorage } from '@peertube/peertube-models'
|
||||
import { sha256 } from '@peertube/peertube-node-utils'
|
||||
import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models/index.js'
|
||||
import { ensureDir, move, outputJSON, remove } from 'fs-extra/esm'
|
||||
|
@ -100,7 +100,7 @@ function updateMasterHLSPlaylist (video: MVideo, playlistArg: MStreamingPlaylist
|
|||
|
||||
logger.info('Updating %s master playlist file of video %s', masterPlaylistPath, video.uuid, lTags(video.uuid))
|
||||
|
||||
if (playlist.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
if (playlist.storage === FileStorage.OBJECT_STORAGE) {
|
||||
playlist.playlistUrl = await storeHLSFileFromFilename(playlist, playlist.playlistFilename)
|
||||
await remove(masterPlaylistPath)
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ function updateSha256VODSegments (video: MVideo, playlistArg: MStreamingPlaylist
|
|||
const outputPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.segmentsSha256Filename)
|
||||
await outputJSON(outputPath, json)
|
||||
|
||||
if (playlist.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
if (playlist.storage === FileStorage.OBJECT_STORAGE) {
|
||||
playlist.segmentsSha256Url = await storeHLSFileFromFilename(playlist, playlist.segmentsSha256Filename)
|
||||
await remove(outputPath)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,10 @@ async function processActivityPubFollow (job: Job) {
|
|||
const payload = job.data as ActivitypubFollowPayload
|
||||
const host = payload.host
|
||||
|
||||
const handle = host
|
||||
? `${payload.name}@${host}`
|
||||
: payload.name
|
||||
|
||||
logger.info('Processing ActivityPub follow in job %s.', job.id)
|
||||
|
||||
let targetActor: MActorFull
|
||||
|
@ -30,14 +34,24 @@ async function processActivityPubFollow (job: Job) {
|
|||
|
||||
let actorUrl: string
|
||||
|
||||
try {
|
||||
if (!payload.name) actorUrl = await getApplicationActorOfHost(sanitizedHost)
|
||||
if (!actorUrl) actorUrl = await loadActorUrlOrGetFromWebfinger((payload.name || SERVER_ACTOR_NAME) + '@' + sanitizedHost)
|
||||
|
||||
targetActor = await getOrCreateAPActor(actorUrl, 'all')
|
||||
} catch (err) {
|
||||
logger.warn(`Do not follow ${handle} because we could not find the actor URL (in database or using webfinger)`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetActor) {
|
||||
logger.warn(`Do not follow ${handle} because we could not fetch/load the actor`)
|
||||
return
|
||||
}
|
||||
|
||||
if (payload.assertIsChannel && !targetActor.VideoChannel) {
|
||||
logger.warn('Do not follow %s@%s because it is not a channel.', payload.name, host)
|
||||
logger.warn(`Do not follow ${handle} because it is not a channel.`)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { Job } from 'bullmq'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { CreateUserExportPayload } from '@peertube/peertube-models'
|
||||
import { UserExportModel } from '@server/models/user/user-export.js'
|
||||
import { UserExporter } from '@server/lib/user-import-export/user-exporter.js'
|
||||
import { Emailer } from '@server/lib/emailer.js'
|
||||
|
||||
const lTags = loggerTagsFactory('user-export')
|
||||
|
||||
export async function processCreateUserExport (job: Job): Promise<void> {
|
||||
const payload = job.data as CreateUserExportPayload
|
||||
const exportModel = await UserExportModel.load(payload.userExportId)
|
||||
|
||||
logger.info('Processing create user export %s in job %s.', payload.userExportId, job.id, lTags())
|
||||
|
||||
if (!exportModel) {
|
||||
logger.info(`User export ${payload.userExportId} does not exist anymore, do not create user export.`, lTags())
|
||||
return
|
||||
}
|
||||
|
||||
const exporter = new UserExporter()
|
||||
|
||||
try {
|
||||
await exporter.export(exportModel)
|
||||
|
||||
await Emailer.Instance.addUserExportCompletedOrErroredJob(exportModel)
|
||||
|
||||
logger.info(`User export ${payload.userExportId} has been created`, lTags())
|
||||
} catch (err) {
|
||||
await Emailer.Instance.addUserExportCompletedOrErroredJob(exportModel)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { Job } from 'bullmq'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { ImportUserArchivePayload } from '@peertube/peertube-models'
|
||||
import { UserImportModel } from '@server/models/user/user-import.js'
|
||||
import { UserImporter } from '@server/lib/user-import-export/user-importer.js'
|
||||
import { Emailer } from '@server/lib/emailer.js'
|
||||
|
||||
const lTags = loggerTagsFactory('user-import')
|
||||
|
||||
export async function processImportUserArchive (job: Job): Promise<void> {
|
||||
const payload = job.data as ImportUserArchivePayload
|
||||
const importModel = await UserImportModel.load(payload.userImportId)
|
||||
|
||||
logger.info(`Processing importing user archive ${payload.userImportId} in job ${job.id}`, lTags())
|
||||
|
||||
if (!importModel) {
|
||||
logger.info(`User import ${payload.userImportId} does not exist anymore, do not create import data.`, lTags())
|
||||
return
|
||||
}
|
||||
|
||||
const exporter = new UserImporter()
|
||||
await exporter.import(importModel)
|
||||
|
||||
try {
|
||||
await Emailer.Instance.addUserImportSuccessJob(importModel)
|
||||
|
||||
logger.info(`User import ${payload.userImportId} ended`, lTags())
|
||||
} catch (err) {
|
||||
await Emailer.Instance.addUserImportErroredJob(importModel)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { Job } from 'bullmq'
|
||||
import { join } from 'path'
|
||||
import { MoveStoragePayload, VideoStateType, VideoStorage } from '@peertube/peertube-models'
|
||||
import { MoveStoragePayload, VideoStateType, FileStorage } from '@peertube/peertube-models'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { updateTorrentMetadata } from '@server/helpers/webtorrent.js'
|
||||
import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants.js'
|
||||
|
@ -52,7 +52,7 @@ export async function onMoveToFileSystemFailure (job: Job, err: any) {
|
|||
|
||||
async function moveWebVideoFiles (video: MVideoWithAllFiles) {
|
||||
for (const file of video.VideoFiles) {
|
||||
if (file.storage === VideoStorage.FILE_SYSTEM) continue
|
||||
if (file.storage === FileStorage.FILE_SYSTEM) continue
|
||||
|
||||
await makeWebVideoFileAvailable(file.filename, VideoPathManager.Instance.getFSVideoFileOutputPath(video, file))
|
||||
await onFileMoved({
|
||||
|
@ -68,7 +68,7 @@ async function moveHLSFiles (video: MVideoWithAllFiles) {
|
|||
const playlistWithVideo = playlist.withVideo(video)
|
||||
|
||||
for (const file of playlist.VideoFiles) {
|
||||
if (file.storage === VideoStorage.FILE_SYSTEM) continue
|
||||
if (file.storage === FileStorage.FILE_SYSTEM) continue
|
||||
|
||||
// Resolution playlist
|
||||
const playlistFilename = getHlsResolutionPlaylistFilename(file.filename)
|
||||
|
@ -97,7 +97,7 @@ async function onFileMoved (options: {
|
|||
const oldFileUrl = file.fileUrl
|
||||
|
||||
file.fileUrl = null
|
||||
file.storage = VideoStorage.FILE_SYSTEM
|
||||
file.storage = FileStorage.FILE_SYSTEM
|
||||
|
||||
await updateTorrentMetadata(videoOrPlaylist, file)
|
||||
await file.save()
|
||||
|
@ -114,7 +114,7 @@ async function doAfterLastMove (options: {
|
|||
const { video, previousVideoState, isNewVideo } = options
|
||||
|
||||
for (const playlist of video.VideoStreamingPlaylists) {
|
||||
if (playlist.storage === VideoStorage.FILE_SYSTEM) continue
|
||||
if (playlist.storage === FileStorage.FILE_SYSTEM) continue
|
||||
|
||||
const playlistWithVideo = playlist.withVideo(video)
|
||||
|
||||
|
@ -124,7 +124,7 @@ async function doAfterLastMove (options: {
|
|||
|
||||
playlist.playlistUrl = null
|
||||
playlist.segmentsSha256Url = null
|
||||
playlist.storage = VideoStorage.FILE_SYSTEM
|
||||
playlist.storage = FileStorage.FILE_SYSTEM
|
||||
|
||||
playlist.assignP2PMediaLoaderInfoHashes(video, playlist.VideoFiles)
|
||||
playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Job } from 'bullmq'
|
||||
import { remove } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { MoveStoragePayload, VideoStateType, VideoStorage } from '@peertube/peertube-models'
|
||||
import { MoveStoragePayload, VideoStateType, FileStorage } from '@peertube/peertube-models'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { updateTorrentMetadata } from '@server/helpers/webtorrent.js'
|
||||
import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants.js'
|
||||
|
@ -45,7 +45,7 @@ export async function onMoveToObjectStorageFailure (job: Job, err: any) {
|
|||
|
||||
async function moveWebVideoFiles (video: MVideoWithAllFiles) {
|
||||
for (const file of video.VideoFiles) {
|
||||
if (file.storage !== VideoStorage.FILE_SYSTEM) continue
|
||||
if (file.storage !== FileStorage.FILE_SYSTEM) continue
|
||||
|
||||
const fileUrl = await storeWebVideoFile(video, file)
|
||||
|
||||
|
@ -59,7 +59,7 @@ async function moveHLSFiles (video: MVideoWithAllFiles) {
|
|||
const playlistWithVideo = playlist.withVideo(video)
|
||||
|
||||
for (const file of playlist.VideoFiles) {
|
||||
if (file.storage !== VideoStorage.FILE_SYSTEM) continue
|
||||
if (file.storage !== FileStorage.FILE_SYSTEM) continue
|
||||
|
||||
// Resolution playlist
|
||||
const playlistFilename = getHlsResolutionPlaylistFilename(file.filename)
|
||||
|
@ -84,7 +84,7 @@ async function onFileMoved (options: {
|
|||
const { videoOrPlaylist, file, fileUrl, oldPath } = options
|
||||
|
||||
file.fileUrl = fileUrl
|
||||
file.storage = VideoStorage.OBJECT_STORAGE
|
||||
file.storage = FileStorage.OBJECT_STORAGE
|
||||
|
||||
await updateTorrentMetadata(videoOrPlaylist, file)
|
||||
await file.save()
|
||||
|
@ -101,13 +101,13 @@ async function doAfterLastMove (options: {
|
|||
const { video, previousVideoState, isNewVideo } = options
|
||||
|
||||
for (const playlist of video.VideoStreamingPlaylists) {
|
||||
if (playlist.storage === VideoStorage.OBJECT_STORAGE) continue
|
||||
if (playlist.storage === FileStorage.OBJECT_STORAGE) continue
|
||||
|
||||
const playlistWithVideo = playlist.withVideo(video)
|
||||
|
||||
playlist.playlistUrl = await storeHLSFileFromFilename(playlistWithVideo, playlist.playlistFilename)
|
||||
playlist.segmentsSha256Url = await storeHLSFileFromFilename(playlistWithVideo, playlist.segmentsSha256Filename)
|
||||
playlist.storage = VideoStorage.OBJECT_STORAGE
|
||||
playlist.storage = FileStorage.OBJECT_STORAGE
|
||||
|
||||
playlist.assignP2PMediaLoaderInfoHashes(video, playlist.VideoFiles)
|
||||
playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { Job } from 'bullmq'
|
||||
import { copy } from 'fs-extra/esm'
|
||||
import { stat } from 'fs/promises'
|
||||
import { VideoFileImportPayload, VideoStorage } from '@peertube/peertube-models'
|
||||
import { VideoFileImportPayload, FileStorage } from '@peertube/peertube-models'
|
||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
|
||||
import { generateWebVideoFilename } from '@server/lib/paths.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import { buildMoveJob } from '@server/lib/video.js'
|
||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MVideoFullLight } from '@server/types/models/index.js'
|
||||
|
@ -15,6 +14,7 @@ import { getLowercaseExtension } from '@peertube/peertube-node-utils'
|
|||
import { getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { JobQueue } from '../job-queue.js'
|
||||
import { buildMoveJob } from '@server/lib/video-jobs.js'
|
||||
|
||||
async function processVideoFileImport (job: Job) {
|
||||
const payload = job.data as VideoFileImportPayload
|
||||
|
@ -68,7 +68,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
|
|||
resolution,
|
||||
extname: fileExt,
|
||||
filename: generateWebVideoFilename(resolution, fileExt),
|
||||
storage: VideoStorage.FILE_SYSTEM,
|
||||
storage: FileStorage.FILE_SYSTEM,
|
||||
size,
|
||||
fps,
|
||||
videoId: video.id
|
||||
|
|
|
@ -22,10 +22,10 @@ import { generateWebVideoFilename } from '@server/lib/paths.js'
|
|||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
|
||||
import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job.js'
|
||||
import { isAbleToUploadVideo } from '@server/lib/user.js'
|
||||
import { isUserQuotaValid } from '@server/lib/user.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import { buildNextVideoState } from '@server/lib/video-state.js'
|
||||
import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video.js'
|
||||
import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
|
||||
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||
import { MVideoImport, MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import.js'
|
||||
import { getLowercaseExtension } from '@peertube/peertube-node-utils'
|
||||
|
@ -138,7 +138,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
|||
|
||||
// Get information about this video
|
||||
const stats = await stat(tempVideoPath)
|
||||
const isAble = await isAbleToUploadVideo(videoImport.User.id, stats.size)
|
||||
const isAble = await isUserQuotaValid({ userId: videoImport.User.id, uploadSize: stats.size })
|
||||
if (isAble === false) {
|
||||
throw new Error('The user video quota is exceeded with this video to import.')
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoS
|
|||
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||
import { JobQueue } from '../job-queue.js'
|
||||
import { isVideoInPublicDirectory } from '@server/lib/video-privacy.js'
|
||||
import { buildStoryboardJobIfNeeded } from '@server/lib/video.js'
|
||||
import { buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
|
||||
|
||||
const lTags = loggerTagsFactory('live', 'job')
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { join } from 'path'
|
|||
import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg/index.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles.js'
|
||||
import { isAbleToUploadVideo } from '@server/lib/user.js'
|
||||
import { isUserQuotaValid } from '@server/lib/user.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import { approximateIntroOutroAdditionalSize, onVideoStudioEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
|
@ -170,7 +170,7 @@ async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoStud
|
|||
const filePathFinder = (i: number) => (payload.tasks[i] as VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload).options.file
|
||||
|
||||
const additionalBytes = await approximateIntroOutroAdditionalSize(video, payload.tasks, filePathFinder)
|
||||
if (await isAbleToUploadVideo(user.id, additionalBytes) === false) {
|
||||
if (await isUserQuotaValid({ userId: user.id, uploadSize: additionalBytes }) === false) {
|
||||
throw new Error('Quota exceeded for this user to edit the video')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,12 @@ import {
|
|||
ActivitypubHttpUnicastPayload,
|
||||
ActorKeysPayload,
|
||||
AfterVideoChannelImportPayload,
|
||||
CreateUserExportPayload,
|
||||
DeleteResumableUploadMetaFilePayload,
|
||||
EmailPayload,
|
||||
FederateVideoPayload,
|
||||
GenerateStoryboardPayload,
|
||||
ImportUserArchivePayload,
|
||||
JobState,
|
||||
JobType,
|
||||
ManageVideoTorrentPayload,
|
||||
|
@ -71,6 +73,8 @@ import { processVideoStudioEdition } from './handlers/video-studio-edition.js'
|
|||
import { processVideoTranscoding } from './handlers/video-transcoding.js'
|
||||
import { processVideosViewsStats } from './handlers/video-views-stats.js'
|
||||
import { onMoveToFileSystemFailure, processMoveToFileSystem } from './handlers/move-to-file-system.js'
|
||||
import { processCreateUserExport } from './handlers/create-user-export.js'
|
||||
import { processImportUserArchive } from './handlers/import-user-archive.js'
|
||||
|
||||
export type CreateJobArgument =
|
||||
{ type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
|
||||
|
@ -98,7 +102,9 @@ export type CreateJobArgument =
|
|||
{ type: 'after-video-channel-import', payload: AfterVideoChannelImportPayload } |
|
||||
{ type: 'notify', payload: NotifyPayload } |
|
||||
{ type: 'federate-video', payload: FederateVideoPayload } |
|
||||
{ type: 'generate-video-storyboard', payload: GenerateStoryboardPayload }
|
||||
{ type: 'create-user-export', payload: CreateUserExportPayload } |
|
||||
{ type: 'generate-video-storyboard', payload: GenerateStoryboardPayload } |
|
||||
{ type: 'import-user-archive', payload: ImportUserArchivePayload }
|
||||
|
||||
export type CreateJobOptions = {
|
||||
delay?: number
|
||||
|
@ -131,7 +137,9 @@ const handlers: { [id in JobType]: (job: Job) => Promise<any> } = {
|
|||
'video-studio-edition': processVideoStudioEdition,
|
||||
'video-transcoding': processVideoTranscoding,
|
||||
'videos-views-stats': processVideosViewsStats,
|
||||
'generate-video-storyboard': processGenerateStoryboard
|
||||
'generate-video-storyboard': processGenerateStoryboard,
|
||||
'create-user-export': processCreateUserExport,
|
||||
'import-user-archive': processImportUserArchive
|
||||
}
|
||||
|
||||
const errorHandlers: { [id in JobType]?: (job: Job, err: any) => Promise<any> } = {
|
||||
|
@ -164,7 +172,9 @@ const jobTypes: JobType[] = [
|
|||
'video-redundancy',
|
||||
'video-studio-edition',
|
||||
'video-transcoding',
|
||||
'videos-views-stats'
|
||||
'videos-views-stats',
|
||||
'create-user-export',
|
||||
'import-user-archive'
|
||||
]
|
||||
|
||||
const silentFailure = new Set<JobType>([ 'activitypub-http-unicast' ])
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { pathExists, remove } from 'fs-extra/esm'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { basename, join } from 'path'
|
||||
import { LiveVideoLatencyMode, LiveVideoLatencyModeType, VideoStorage } from '@peertube/peertube-models'
|
||||
import { LiveVideoLatencyMode, LiveVideoLatencyModeType, FileStorage } from '@peertube/peertube-models'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { VIDEO_LIVE } from '@server/initializers/constants.js'
|
||||
import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/types/models/index.js'
|
||||
|
@ -24,7 +24,7 @@ async function cleanupUnsavedNormalLive (video: MVideo, streamingPlaylist: MStre
|
|||
const hlsDirectory = getLiveDirectory(video)
|
||||
|
||||
// We uploaded files to object storage too, remove them
|
||||
if (streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
if (streamingPlaylist.storage === FileStorage.OBJECT_STORAGE) {
|
||||
await removeHLSObjectStorage(streamingPlaylist.withVideo(video))
|
||||
}
|
||||
|
||||
|
@ -86,7 +86,7 @@ async function cleanupTMPLiveFilesFromFilesystem (video: MVideo) {
|
|||
}
|
||||
|
||||
async function cleanupTMPLiveFilesFromObjectStorage (streamingPlaylist: MStreamingPlaylistVideo) {
|
||||
if (streamingPlaylist.storage !== VideoStorage.OBJECT_STORAGE) return
|
||||
if (streamingPlaylist.storage !== FileStorage.OBJECT_STORAGE) return
|
||||
|
||||
logger.info('Cleanup TMP live files from object storage for %s.', streamingPlaylist.Video.uuid)
|
||||
|
||||
|
|
|
@ -14,14 +14,14 @@ import { removeHLSFileObjectStorageByPath, storeHLSFileFromContent, storeHLSFile
|
|||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist.js'
|
||||
import { MStreamingPlaylistVideo, MUserId, MVideoLiveVideo } from '@server/types/models/index.js'
|
||||
import { LiveVideoError, VideoStorage, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||
import { LiveVideoError, FileStorage, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||
import {
|
||||
generateHLSMasterPlaylistFilename,
|
||||
generateHlsSha256SegmentsFilename,
|
||||
getLiveDirectory,
|
||||
getLiveReplayBaseDirectory
|
||||
} from '../../paths.js'
|
||||
import { isAbleToUploadVideo } from '../../user.js'
|
||||
import { isUserQuotaValid } from '../../user.js'
|
||||
import { LiveQuotaStore } from '../live-quota-store.js'
|
||||
import { LiveSegmentShaStore } from '../live-segment-sha-store.js'
|
||||
import { buildConcatenatedName, getLiveSegmentTime } from '../live-utils.js'
|
||||
|
@ -95,7 +95,7 @@ class MuxingSession extends EventEmitter {
|
|||
private aborted = false
|
||||
|
||||
private readonly isAbleToUploadVideoWithCache = memoizee((userId: number) => {
|
||||
return isAbleToUploadVideo(userId, 1000)
|
||||
return isUserQuotaValid({ userId, uploadSize: 1000 })
|
||||
}, { maxAge: MEMOIZE_TTL.LIVE_ABLE_TO_UPLOAD })
|
||||
|
||||
private readonly hasClientSocketInBadHealthWithCache = memoizee((sessionId: string) => {
|
||||
|
@ -186,7 +186,7 @@ class MuxingSession extends EventEmitter {
|
|||
if (this.masterPlaylistCreated === true) return
|
||||
|
||||
try {
|
||||
if (this.streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
if (this.streamingPlaylist.storage === FileStorage.OBJECT_STORAGE) {
|
||||
let masterContent = await readFile(path, 'utf-8')
|
||||
|
||||
// If the disk sync is slow, don't upload an empty master playlist on object storage
|
||||
|
@ -260,7 +260,7 @@ class MuxingSession extends EventEmitter {
|
|||
logger.warn('Cannot remove segment sha %s from sha store', segmentPath, { err, ...this.lTags() })
|
||||
}
|
||||
|
||||
if (this.streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
if (this.streamingPlaylist.storage === FileStorage.OBJECT_STORAGE) {
|
||||
try {
|
||||
await removeHLSFileObjectStorageByPath(this.streamingPlaylist, segmentPath)
|
||||
} catch (err) {
|
||||
|
@ -345,7 +345,7 @@ class MuxingSession extends EventEmitter {
|
|||
await this.addSegmentToReplay(segmentPath)
|
||||
}
|
||||
|
||||
if (this.streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) {
|
||||
if (this.streamingPlaylist.storage === FileStorage.OBJECT_STORAGE) {
|
||||
try {
|
||||
await storeHLSFileFromPath(this.streamingPlaylist, segmentPath)
|
||||
|
||||
|
@ -464,8 +464,8 @@ class MuxingSession extends EventEmitter {
|
|||
playlist.type = VideoStreamingPlaylistType.HLS
|
||||
|
||||
playlist.storage = CONFIG.OBJECT_STORAGE.ENABLED
|
||||
? VideoStorage.OBJECT_STORAGE
|
||||
: VideoStorage.FILE_SYSTEM
|
||||
? FileStorage.OBJECT_STORAGE
|
||||
: FileStorage.FILE_SYSTEM
|
||||
|
||||
return playlist.save()
|
||||
}
|
||||
|
|
|
@ -30,13 +30,16 @@ export function buildActorInstance (type: ActivityPubActorType, url: string, pre
|
|||
}) as MActor
|
||||
}
|
||||
|
||||
export async function updateLocalActorImageFiles (
|
||||
accountOrChannel: MAccountDefault | MChannelDefault,
|
||||
imagePhysicalFile: Express.Multer.File,
|
||||
export async function updateLocalActorImageFiles (options: {
|
||||
accountOrChannel: MAccountDefault | MChannelDefault
|
||||
imagePhysicalFile: { path: string }
|
||||
type: ActorImageType_Type
|
||||
) {
|
||||
sendActorUpdate: boolean
|
||||
}) {
|
||||
const { accountOrChannel, imagePhysicalFile, type, sendActorUpdate } = options
|
||||
|
||||
const processImageSize = async (imageSize: { width: number, height: number }) => {
|
||||
const extension = getLowercaseExtension(imagePhysicalFile.filename)
|
||||
const extension = getLowercaseExtension(imagePhysicalFile.path)
|
||||
|
||||
const imageName = buildUUID() + extension
|
||||
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES_DIR, imageName)
|
||||
|
@ -63,7 +66,9 @@ export async function updateLocalActorImageFiles (
|
|||
const updatedActor = await updateActorImages(accountOrChannel.Actor, type, actorImagesInfo, t)
|
||||
await updatedActor.save({ transaction: t })
|
||||
|
||||
if (sendActorUpdate) {
|
||||
await sendUpdateActor(accountOrChannel, t)
|
||||
}
|
||||
|
||||
return type === ActorImageType.AVATAR
|
||||
? updatedActor.Avatars
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import {
|
||||
MVideoAccountLightBlacklistAllFiles,
|
||||
|
@ -7,6 +8,7 @@ import {
|
|||
MVideoImmutable,
|
||||
MVideoThumbnail
|
||||
} from '@server/types/models/index.js'
|
||||
import { getOrCreateAPVideo } from '../activitypub/videos/get.js'
|
||||
|
||||
type VideoLoadType = 'for-api' | 'all' | 'only-video' | 'id' | 'none' | 'only-immutable-attributes'
|
||||
|
||||
|
@ -50,17 +52,36 @@ function loadVideoByUrl (
|
|||
url: string,
|
||||
fetchType: VideoLoadByUrlType
|
||||
): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
|
||||
if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccount(url)
|
||||
if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccountAndFiles(url)
|
||||
|
||||
if (fetchType === 'only-immutable-attributes') return VideoModel.loadByUrlImmutableAttributes(url)
|
||||
|
||||
if (fetchType === 'only-video') return VideoModel.loadByUrl(url)
|
||||
}
|
||||
|
||||
async function loadOrCreateVideoIfAllowedForUser (videoUrl: string) {
|
||||
if (CONFIG.SEARCH.REMOTE_URI.USERS) {
|
||||
try {
|
||||
const res = await getOrCreateAPVideo({
|
||||
videoObject: videoUrl,
|
||||
fetchType: 'only-immutable-attributes',
|
||||
allowRefresh: false
|
||||
})
|
||||
|
||||
return res?.video
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
return VideoModel.loadByUrlImmutableAttributes(videoUrl)
|
||||
}
|
||||
|
||||
export {
|
||||
type VideoLoadType,
|
||||
type VideoLoadByUrlType,
|
||||
|
||||
loadVideo,
|
||||
loadVideoByUrl
|
||||
loadVideoByUrl,
|
||||
loadOrCreateVideoIfAllowedForUser
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
MCommentAbuseAccountVideo,
|
||||
MCommentOwnerVideo,
|
||||
MUser,
|
||||
MUserDefault,
|
||||
MVideoAbuseVideoFull,
|
||||
MVideoAccountLightBlacklistAllFiles
|
||||
} from '@server/types/models/index.js'
|
||||
|
@ -38,7 +39,7 @@ export type AcceptResult = {
|
|||
function isLocalVideoFileAccepted (object: {
|
||||
videoBody: VideoCreate
|
||||
videoFile: VideoUploadFile
|
||||
user: UserModel
|
||||
user: MUserDefault
|
||||
}): AcceptResult {
|
||||
return { accepted: true }
|
||||
}
|
||||
|
|
|
@ -13,8 +13,13 @@ function generateWebVideoObjectStorageKey (filename: string) {
|
|||
return filename
|
||||
}
|
||||
|
||||
function generateUserExportObjectStorageKey (filename: string) {
|
||||
return filename
|
||||
}
|
||||
|
||||
export {
|
||||
generateHLSObjectStorageKey,
|
||||
generateHLSObjectBaseStorageKey,
|
||||
generateWebVideoObjectStorageKey
|
||||
generateWebVideoObjectStorageKey,
|
||||
generateUserExportObjectStorageKey
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue