Init commit
This commit is contained in:
228
server/core/models/user/user-export.ts
Normal file
228
server/core/models/user/user-export.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { FileStorage, UserExportState, type FileStorageType, type UserExport, type UserExportStateType } from '@peertube/peertube-models'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import {
|
||||
JWT_TOKEN_USER_EXPORT_FILE_LIFETIME,
|
||||
DOWNLOAD_PATHS,
|
||||
USER_EXPORT_FILE_PREFIX,
|
||||
USER_EXPORT_STATES,
|
||||
WEBSERVER
|
||||
} from '@server/initializers/constants.js'
|
||||
import { removeUserExportObjectStorage } from '@server/lib/object-storage/user-export.js'
|
||||
import { getFSUserExportFilePath } from '@server/lib/paths.js'
|
||||
import { MUserAccountId, MUserExport } from '@server/types/models/index.js'
|
||||
import { remove } from 'fs-extra/esm'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { join } from 'path'
|
||||
import { FindOptions, Op } from 'sequelize'
|
||||
import { AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { doesExist } from '../shared/query.js'
|
||||
import { SequelizeModel } from '../shared/sequelize-type.js'
|
||||
import { getSort } from '../shared/sort.js'
|
||||
import { UserModel } from './user.js'
|
||||
|
||||
@Table({
|
||||
tableName: 'userExport',
|
||||
indexes: [
|
||||
{
|
||||
fields: [ 'userId' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'filename' ],
|
||||
unique: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class UserExportModel extends SequelizeModel<UserExportModel> {
|
||||
@CreatedAt
|
||||
declare createdAt: Date
|
||||
|
||||
@UpdatedAt
|
||||
declare updatedAt: Date
|
||||
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
declare filename: string
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
declare withVideoFiles: boolean
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
declare state: UserExportStateType
|
||||
|
||||
@AllowNull(true)
|
||||
@Column(DataType.TEXT)
|
||||
declare error: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Column(DataType.BIGINT)
|
||||
declare size: number
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
declare storage: FileStorageType
|
||||
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
declare fileUrl: string
|
||||
|
||||
@ForeignKey(() => UserModel)
|
||||
@Column
|
||||
declare userId: number
|
||||
|
||||
@BelongsTo(() => UserModel, {
|
||||
foreignKey: {
|
||||
allowNull: false
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
declare User: Awaited<UserModel>
|
||||
|
||||
@BeforeDestroy
|
||||
static removeFile (instance: UserExportModel) {
|
||||
logger.info('Removing user export file %s.', instance.filename)
|
||||
|
||||
if (instance.storage === FileStorage.FILE_SYSTEM) {
|
||||
remove(getFSUserExportFilePath(instance))
|
||||
.catch(err => logger.error('Cannot delete user export archive %s from filesystem.', instance.filename, { err }))
|
||||
} else {
|
||||
removeUserExportObjectStorage(instance)
|
||||
.catch(err => logger.error('Cannot delete user export archive %s from object storage.', instance.filename, { err }))
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
static listByUser (user: MUserAccountId) {
|
||||
const query: FindOptions = {
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
}
|
||||
|
||||
return UserExportModel.findAll<MUserExport>(query)
|
||||
}
|
||||
|
||||
static listExpired (expirationTimeMS: number) {
|
||||
const query: FindOptions = {
|
||||
where: {
|
||||
createdAt: {
|
||||
[Op.lt]: new Date(new Date().getTime() - expirationTimeMS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UserExportModel.findAll<MUserExport>(query)
|
||||
}
|
||||
|
||||
static listForApi (options: {
|
||||
user: MUserAccountId
|
||||
start: number
|
||||
count: number
|
||||
}) {
|
||||
const { count, start, user } = options
|
||||
|
||||
const query: FindOptions = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getSort('createdAt'),
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
UserExportModel.count(query),
|
||||
UserExportModel.findAll<MUserExport>(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static load (id: number | string) {
|
||||
return UserExportModel.findByPk<MUserExport>(id)
|
||||
}
|
||||
|
||||
static loadByFilename (filename: string) {
|
||||
return UserExportModel.findOne<MUserExport>({ where: { filename } })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static async doesOwnedFileExist (filename: string, storage: FileStorageType) {
|
||||
const query = 'SELECT 1 FROM "userExport" ' +
|
||||
`WHERE "filename" = $filename AND "storage" = $storage LIMIT 1`
|
||||
|
||||
return doesExist({ sequelize: this.sequelize, query, bind: { filename, storage } })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
generateAndSetFilename () {
|
||||
if (!this.userId) throw new Error('Cannot generate filename without userId')
|
||||
if (!this.createdAt) throw new Error('Cannot generate filename without createdAt')
|
||||
|
||||
this.filename = `${USER_EXPORT_FILE_PREFIX}${this.userId}-${this.createdAt.toISOString()}.zip`
|
||||
}
|
||||
|
||||
canBeSafelyRemoved () {
|
||||
const supportedStates = new Set<UserExportStateType>([ UserExportState.COMPLETED, UserExportState.ERRORED, UserExportState.PENDING ])
|
||||
|
||||
return supportedStates.has(this.state)
|
||||
}
|
||||
|
||||
generateJWT () {
|
||||
return jwt.sign(
|
||||
{
|
||||
userExportId: this.id
|
||||
},
|
||||
CONFIG.SECRETS.PEERTUBE,
|
||||
{
|
||||
expiresIn: JWT_TOKEN_USER_EXPORT_FILE_LIFETIME,
|
||||
audience: this.filename,
|
||||
issuer: WEBSERVER.URL
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
isJWTValid (jwtToken: string) {
|
||||
try {
|
||||
const payload = jwt.verify(jwtToken, CONFIG.SECRETS.PEERTUBE, {
|
||||
audience: this.filename,
|
||||
issuer: WEBSERVER.URL
|
||||
})
|
||||
|
||||
if ((payload as any).userExportId !== this.id) return false
|
||||
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
getFileDownloadUrl () {
|
||||
if (this.state !== UserExportState.COMPLETED) return null
|
||||
|
||||
return WEBSERVER.URL + join(DOWNLOAD_PATHS.USER_EXPORTS, this.filename) + '?jwt=' + this.generateJWT()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toFormattedJSON (this: MUserExport): UserExport {
|
||||
return {
|
||||
id: this.id,
|
||||
|
||||
state: {
|
||||
id: this.state,
|
||||
label: USER_EXPORT_STATES[this.state]
|
||||
},
|
||||
|
||||
size: this.size,
|
||||
|
||||
fileUrl: this.fileUrl,
|
||||
privateDownloadUrl: this.getFileDownloadUrl(),
|
||||
createdAt: this.createdAt.toISOString(),
|
||||
expiresOn: new Date(this.createdAt.getTime() + CONFIG.EXPORT.USERS.EXPORT_EXPIRATION).toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user