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'
|
||||