Agnostic actor image storage

This commit is contained in:
Chocobozzz 2021-04-06 11:35:56 +02:00 committed by Chocobozzz
parent 968aaed206
commit f479685678
36 changed files with 186 additions and 147 deletions

View File

@ -1,7 +1,7 @@
import { Account } from '@app/shared/shared-main/account/account.model'
import { hasUserRight } from '@shared/core-utils/users'
import {
Avatar,
ActorImage,
NSFWPolicyType,
User as UserServerModel,
UserAdminFlag,
@ -131,7 +131,7 @@ export class User implements UserServerModel {
}
}
updateAccountAvatar (newAccountAvatar?: Avatar) {
updateAccountAvatar (newAccountAvatar?: ActorImage) {
if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar)
else this.account.resetAvatar()
}

View File

@ -7,8 +7,7 @@ import { AuthService } from '@app/core/auth'
import { getBytes } from '@root-helpers/bytes'
import { UserLocalStorageKeys } from '@root-helpers/users'
import {
Avatar,
NSFWPolicyType,
ActorImage,
ResultList,
User as UserServerModel,
UserCreate,
@ -136,7 +135,7 @@ export class UserService {
changeAvatar (avatarForm: FormData) {
const url = UserService.BASE_USERS_URL + 'me/avatar/pick'
return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm)
return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm)
.pipe(catchError(err => this.restExtractor.handleError(err)))
}

View File

@ -1,4 +1,4 @@
import { Account as ServerAccount, Avatar } from '@shared/models'
import { Account as ServerAccount, ActorImage } from '@shared/models'
import { Actor } from './actor.model'
export class Account extends Actor implements ServerAccount {
@ -38,7 +38,7 @@ export class Account extends Actor implements ServerAccount {
this.mutedServerByInstance = false
}
updateAvatar (newAvatar: Avatar) {
updateAvatar (newAvatar: ActorImage) {
this.avatar = newAvatar
this.updateComputedAttributes()

View File

@ -1,4 +1,4 @@
import { Actor as ActorServer, Avatar } from '@shared/models'
import { Actor as ActorServer, ActorImage } from '@shared/models'
import { getAbsoluteAPIUrl } from '@app/helpers'
export abstract class Actor implements ActorServer {
@ -10,7 +10,7 @@ export abstract class Actor implements ActorServer {
followersCount: number
createdAt: Date | string
updatedAt: Date | string
avatar: Avatar
avatar: ActorImage
avatarUrl: string

View File

@ -1,4 +1,4 @@
import { Account as ServerAccount, Avatar, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models'
import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models'
import { Account } from '../account/account.model'
import { Actor } from '../account/actor.model'
@ -51,7 +51,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
}
}
updateAvatar (newAvatar: Avatar) {
updateAvatar (newAvatar: ActorImage) {
this.avatar = newAvatar
this.updateComputedAttributes()

View File

@ -3,7 +3,7 @@ import { catchError, map, tap } from 'rxjs/operators'
import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
import { Avatar, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models'
import { ActorImage, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models'
import { environment } from '../../../../environments/environment'
import { Account } from '../account'
import { AccountService } from '../account/account.service'
@ -85,7 +85,7 @@ export class VideoChannelService {
changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) {
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick'
return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm)
return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm)
.pipe(catchError(err => this.restExtractor.handleError(err)))
}

View File

@ -6,7 +6,7 @@ import { Actor } from '@app/shared/shared-main/account/actor.model'
import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model'
import { peertubeTranslate } from '@shared/core-utils/i18n'
import {
Avatar,
ActorImage,
ServerConfig,
UserRight,
Video as VideoServerModel,
@ -72,7 +72,7 @@ export class Video implements VideoServerModel {
displayName: string
url: string
host: string
avatar?: Avatar
avatar?: ActorImage
}
channel: {
@ -81,7 +81,7 @@ export class Video implements VideoServerModel {
displayName: string
url: string
host: string
avatar?: Avatar
avatar?: ActorImage
}
userHistory?: {

View File

@ -11,7 +11,7 @@ import { VideoRedundancyModel } from '../server/models/redundancy/video-redundan
import * as Bluebird from 'bluebird'
import { getUUIDFromFilename } from '../server/helpers/utils'
import { ThumbnailModel } from '../server/models/video/thumbnail'
import { AvatarModel } from '../server/models/avatar/avatar'
import { ActorImageModel } from '../server/models/account/actor-image'
import { uniq, values } from 'lodash'
import { ThumbnailType } from '@shared/models'
@ -43,7 +43,7 @@ async function run () {
await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true, ThumbnailType.PREVIEW)),
await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false, ThumbnailType.MINIATURE)),
await pruneDirectory(CONFIG.STORAGE.AVATARS_DIR, doesAvatarExist)
await pruneDirectory(CONFIG.STORAGE.ACTOR_IMAGES, doesActorImageExist)
)
const tmpFiles = await readdir(CONFIG.STORAGE.TMP_DIR)
@ -107,10 +107,10 @@ function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) {
}
}
async function doesAvatarExist (file: string) {
const avatar = await AvatarModel.loadByName(file)
async function doesActorImageExist (file: string) {
const image = await ActorImageModel.loadByName(file)
return !!avatar
return !!image
}
async function doesRedundancyExist (file: string) {

View File

@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config'
import { MIMETYPES } from '../../../initializers/constants'
import { sequelizeTypescript } from '../../../initializers/database'
import { sendUpdateActor } from '../../../lib/activitypub/send'
import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/avatar'
import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/actor-image'
import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
import {
asyncMiddleware,

View File

@ -13,7 +13,7 @@ import { CONFIG } from '../../initializers/config'
import { MIMETYPES } from '../../initializers/constants'
import { sequelizeTypescript } from '../../initializers/database'
import { sendUpdateActor } from '../../lib/activitypub/send'
import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/avatar'
import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/actor-image'
import { JobQueue } from '../../lib/job-queue'
import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
import {

View File

@ -4,10 +4,10 @@ import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache
import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
import { logger } from '../helpers/logger'
import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar'
import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image'
import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
import { asyncMiddleware } from '../middlewares'
import { AvatarModel } from '../models/avatar/avatar'
import { ActorImageModel } from '../models/account/actor-image'
const lazyStaticRouter = express.Router()
@ -15,7 +15,12 @@ lazyStaticRouter.use(cors())
lazyStaticRouter.use(
LAZY_STATIC_PATHS.AVATARS + ':filename',
asyncMiddleware(getAvatar)
asyncMiddleware(getActorImage)
)
lazyStaticRouter.use(
LAZY_STATIC_PATHS.BANNERS + ':filename',
asyncMiddleware(getActorImage)
)
lazyStaticRouter.use(
@ -43,36 +48,36 @@ export {
// ---------------------------------------------------------------------------
async function getAvatar (req: express.Request, res: express.Response) {
async function getActorImage (req: express.Request, res: express.Response) {
const filename = req.params.filename
if (avatarPathUnsafeCache.has(filename)) {
return res.sendFile(avatarPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER })
if (actorImagePathUnsafeCache.has(filename)) {
return res.sendFile(actorImagePathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER })
}
const avatar = await AvatarModel.loadByName(filename)
if (!avatar) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
const image = await ActorImageModel.loadByName(filename)
if (!image) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
if (avatar.onDisk === false) {
if (!avatar.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
if (image.onDisk === false) {
if (!image.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
logger.info('Lazy serve remote avatar image %s.', avatar.fileUrl)
logger.info('Lazy serve remote actor image %s.', image.fileUrl)
try {
await pushAvatarProcessInQueue({ filename: avatar.filename, fileUrl: avatar.fileUrl })
await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl })
} catch (err) {
logger.warn('Cannot process remote avatar %s.', avatar.fileUrl, { err })
logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err })
return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
}
avatar.onDisk = true
avatar.save()
.catch(err => logger.error('Cannot save new avatar disk state.', { err }))
image.onDisk = true
image.save()
.catch(err => logger.error('Cannot save new actor image disk state.', { err }))
}
const path = avatar.getPath()
const path = image.getPath()
avatarPathUnsafeCache.set(filename, path)
actorImagePathUnsafeCache.set(filename, path)
return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
}

View File

@ -59,7 +59,7 @@ const CONFIG = {
},
STORAGE: {
TMP_DIR: buildPath(config.get<string>('storage.tmp')),
AVATARS_DIR: buildPath(config.get<string>('storage.avatars')),
ACTOR_IMAGES: buildPath(config.get<string>('storage.avatars')),
LOG_DIR: buildPath(config.get<string>('storage.logs')),
VIDEOS_DIR: buildPath(config.get<string>('storage.videos')),
STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')),

View File

@ -580,6 +580,7 @@ const STATIC_DOWNLOAD_PATHS = {
HLS_VIDEOS: '/download/streaming-playlists/hls/videos/'
}
const LAZY_STATIC_PATHS = {
BANNERS: '/lazy-static/banners/',
AVATARS: '/lazy-static/avatars/',
PREVIEWS: '/lazy-static/previews/',
VIDEO_CAPTIONS: '/lazy-static/video-captions/',
@ -634,7 +635,7 @@ const LRU_CACHE = {
USER_TOKENS: {
MAX_SIZE: 1000
},
AVATAR_STATIC: {
ACTOR_IMAGE_STATIC: {
MAX_SIZE: 500
}
}
@ -671,7 +672,7 @@ const MEMOIZE_LENGTH = {
}
const QUEUE_CONCURRENCY = {
AVATAR_PROCESS_IMAGE: 3
ACTOR_PROCESS_IMAGE: 3
}
const REDUNDANCY = {

View File

@ -1,7 +1,7 @@
import { TrackerModel } from '@server/models/server/tracker'
import { VideoTrackerModel } from '@server/models/server/video-tracker'
import { QueryTypes, Transaction } from 'sequelize'
import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
import { TrackerModel } from '@server/models/server/tracker'
import { VideoTrackerModel } from '@server/models/server/video-tracker'
import { isTestInstance } from '../helpers/core-utils'
import { logger } from '../helpers/logger'
import { AbuseModel } from '../models/abuse/abuse'
@ -11,6 +11,7 @@ import { VideoCommentAbuseModel } from '../models/abuse/video-comment-abuse'
import { AccountModel } from '../models/account/account'
import { AccountBlocklistModel } from '../models/account/account-blocklist'
import { AccountVideoRateModel } from '../models/account/account-video-rate'
import { ActorImageModel } from '../models/account/actor-image'
import { UserModel } from '../models/account/user'
import { UserNotificationModel } from '../models/account/user-notification'
import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
@ -18,7 +19,6 @@ import { UserVideoHistoryModel } from '../models/account/user-video-history'
import { ActorModel } from '../models/activitypub/actor'
import { ActorFollowModel } from '../models/activitypub/actor-follow'
import { ApplicationModel } from '../models/application/application'
import { AvatarModel } from '../models/avatar/avatar'
import { OAuthClientModel } from '../models/oauth/oauth-client'
import { OAuthTokenModel } from '../models/oauth/oauth-token'
import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
@ -95,7 +95,7 @@ async function initDatabaseModels (silent: boolean) {
ApplicationModel,
ActorModel,
ActorFollowModel,
AvatarModel,
ActorImageModel,
AccountModel,
OAuthClientModel,
OAuthTokenModel,

View File

@ -19,8 +19,8 @@ import { getUrlFromWebfinger } from '../../helpers/webfinger'
import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
import { sequelizeTypescript } from '../../initializers/database'
import { AccountModel } from '../../models/account/account'
import { ActorImageModel } from '../../models/account/actor-image'
import { ActorModel } from '../../models/activitypub/actor'
import { AvatarModel } from '../../models/avatar/avatar'
import { ServerModel } from '../../models/server/server'
import { VideoChannelModel } from '../../models/video/video-channel'
import {
@ -183,7 +183,7 @@ async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo
}
}
const avatar = await AvatarModel.create({
const avatar = await ActorImageModel.create({
filename: info.name,
onDisk: info.onDisk,
fileUrl: info.fileUrl
@ -378,7 +378,7 @@ function saveActorAndServerAndModelIfNotExist (
// Avatar?
if (result.avatar) {
const avatar = await AvatarModel.create({
const avatar = await ActorImageModel.create({
filename: result.avatar.name,
fileUrl: result.avatar.fileUrl,
onDisk: false

View File

@ -1,17 +1,17 @@
import 'multer'
import { sendUpdateActor } from './activitypub/send'
import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
import { updateActorAvatarInstance, deleteActorAvatarInstance } from './activitypub/actor'
import { processImage } from '../helpers/image-utils'
import { extname, join } from 'path'
import { retryTransactionWrapper } from '../helpers/database-utils'
import { v4 as uuidv4 } from 'uuid'
import { CONFIG } from '../initializers/config'
import { sequelizeTypescript } from '../initializers/database'
import * as LRUCache from 'lru-cache'
import { queue } from 'async'
import * as LRUCache from 'lru-cache'
import { extname, join } from 'path'
import { v4 as uuidv4 } from 'uuid'
import { retryTransactionWrapper } from '../helpers/database-utils'
import { processImage } from '../helpers/image-utils'
import { downloadImage } from '../helpers/requests'
import { CONFIG } from '../initializers/config'
import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
import { sequelizeTypescript } from '../initializers/database'
import { MAccountDefault, MChannelDefault } from '../types/models'
import { deleteActorAvatarInstance, updateActorAvatarInstance } from './activitypub/actor'
import { sendUpdateActor } from './activitypub/send'
async function updateLocalActorAvatarFile (
accountOrChannel: MAccountDefault | MChannelDefault,
@ -20,7 +20,7 @@ async function updateLocalActorAvatarFile (
const extension = extname(avatarPhysicalFile.filename)
const avatarName = uuidv4() + extension
const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, avatarName)
await processImage(avatarPhysicalFile.path, destination, AVATARS_SIZE)
return retryTransactionWrapper(() => {
@ -59,12 +59,12 @@ async function deleteLocalActorAvatarFile (
type DownloadImageQueueTask = { fileUrl: string, filename: string }
const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
downloadImage(task.fileUrl, CONFIG.STORAGE.AVATARS_DIR, task.filename, AVATARS_SIZE)
downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, AVATARS_SIZE)
.then(() => cb())
.catch(err => cb(err))
}, QUEUE_CONCURRENCY.AVATAR_PROCESS_IMAGE)
}, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE)
function pushAvatarProcessInQueue (task: DownloadImageQueueTask) {
function pushActorImageProcessInQueue (task: DownloadImageQueueTask) {
return new Promise<void>((res, rej) => {
downloadImageQueue.push(task, err => {
if (err) return rej(err)
@ -75,11 +75,11 @@ function pushAvatarProcessInQueue (task: DownloadImageQueueTask) {
}
// Unsafe so could returns paths that does not exist anymore
const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVATAR_STATIC.MAX_SIZE })
const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.ACTOR_IMAGE_STATIC.MAX_SIZE })
export {
avatarPathUnsafeCache,
actorImagePathUnsafeCache,
updateLocalActorAvatarFile,
deleteLocalActorAvatarFile,
pushAvatarProcessInQueue
pushActorImageProcessInQueue
}

View File

@ -33,7 +33,7 @@ import {
import { ActorModel } from '../activitypub/actor'
import { ActorFollowModel } from '../activitypub/actor-follow'
import { ApplicationModel } from '../application/application'
import { AvatarModel } from '../avatar/avatar'
import { ActorImageModel } from './actor-image'
import { ServerModel } from '../server/server'
import { ServerBlocklistModel } from '../server/server-blocklist'
import { getSort, throwIfNotValid } from '../utils'
@ -82,7 +82,8 @@ export type SummaryOptions = {
serverInclude,
{
model: AvatarModel.unscoped(),
model: ActorImageModel.unscoped(),
as: 'Avatar',
required: false
}
]

View File

@ -1,16 +1,17 @@
import { remove } from 'fs-extra'
import { join } from 'path'
import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Avatar } from '../../../shared/models/avatars/avatar.model'
import { LAZY_STATIC_PATHS } from '../../initializers/constants'
import { logger } from '../../helpers/logger'
import { remove } from 'fs-extra'
import { CONFIG } from '../../initializers/config'
import { throwIfNotValid } from '../utils'
import { MActorImageFormattable } from '@server/types/models'
import { ActorImageType } from '@shared/models'
import { ActorImage } from '../../../shared/models/actors/actor-image.model'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { MAvatarFormattable } from '@server/types/models'
import { logger } from '../../helpers/logger'
import { CONFIG } from '../../initializers/config'
import { LAZY_STATIC_PATHS } from '../../initializers/constants'
import { throwIfNotValid } from '../utils'
@Table({
tableName: 'avatar',
tableName: 'actorImage',
indexes: [
{
fields: [ 'filename' ],
@ -18,14 +19,14 @@ import { MAvatarFormattable } from '@server/types/models'
}
]
})
export class AvatarModel extends Model {
export class ActorImageModel extends Model {
@AllowNull(false)
@Column
filename: string
@AllowNull(true)
@Is('AvatarFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true))
@Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true))
@Column
fileUrl: string
@ -33,6 +34,10 @@ export class AvatarModel extends Model {
@Column
onDisk: boolean
@AllowNull(false)
@Column
type: ActorImageType
@CreatedAt
createdAt: Date
@ -40,12 +45,12 @@ export class AvatarModel extends Model {
updatedAt: Date
@AfterDestroy
static removeFilesAndSendDelete (instance: AvatarModel) {
logger.info('Removing avatar file %s.', instance.filename)
static removeFilesAndSendDelete (instance: ActorImageModel) {
logger.info('Removing actor image file %s.', instance.filename)
// Don't block the transaction
instance.removeAvatar()
.catch(err => logger.error('Cannot remove avatar file %s.', instance.filename, err))
instance.removeImage()
.catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err))
}
static loadByName (filename: string) {
@ -55,10 +60,10 @@ export class AvatarModel extends Model {
}
}
return AvatarModel.findOne(query)
return ActorImageModel.findOne(query)
}
toFormattedJSON (this: MAvatarFormattable): Avatar {
toFormattedJSON (this: MActorImageFormattable): ActorImage {
return {
path: this.getStaticPath(),
createdAt: this.createdAt,
@ -71,11 +76,11 @@ export class AvatarModel extends Model {
}
getPath () {
return join(CONFIG.STORAGE.AVATARS_DIR, this.filename)
return join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename)
}
removeAvatar () {
const avatarPath = join(CONFIG.STORAGE.AVATARS_DIR, this.filename)
return remove(avatarPath)
removeImage () {
const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename)
return remove(imagePath)
}
}

View File

@ -10,7 +10,6 @@ import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
import { ActorModel } from '../activitypub/actor'
import { ActorFollowModel } from '../activitypub/actor-follow'
import { ApplicationModel } from '../application/application'
import { AvatarModel } from '../avatar/avatar'
import { PluginModel } from '../server/plugin'
import { ServerModel } from '../server/server'
import { getSort, throwIfNotValid } from '../utils'
@ -20,6 +19,7 @@ import { VideoChannelModel } from '../video/video-channel'
import { VideoCommentModel } from '../video/video-comment'
import { VideoImportModel } from '../video/video-import'
import { AccountModel } from './account'
import { ActorImageModel } from './actor-image'
import { UserModel } from './user'
enum ScopeNames {
@ -34,7 +34,8 @@ function buildActorWithAvatarInclude () {
include: [
{
attributes: [ 'filename' ],
model: AvatarModel.unscoped(),
as: 'Avatar',
model: ActorImageModel.unscoped(),
required: false
},
{
@ -172,7 +173,8 @@ function buildAccountInclude (required: boolean, withActor = false) {
},
{
attributes: [ 'filename' ],
model: AvatarModel.unscoped(),
as: 'Avatar',
model: ActorImageModel.unscoped(),
required: false
},
{

View File

@ -19,7 +19,7 @@ import {
} from 'sequelize-typescript'
import { ModelCache } from '@server/models/model-cache'
import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
import { Avatar } from '../../../shared/models/avatars/avatar.model'
import { ActorImage } from '../../../shared/models/actors/actor-image.model'
import { activityPubContextify } from '../../helpers/activitypub'
import {
isActorFollowersCountValid,
@ -43,7 +43,7 @@ import {
MActorWithInboxes
} from '../../types/models'
import { AccountModel } from '../account/account'
import { AvatarModel } from '../avatar/avatar'
import { ActorImageModel } from '../account/actor-image'
import { ServerModel } from '../server/server'
import { isOutdated, throwIfNotValid } from '../utils'
import { VideoModel } from '../video/video'
@ -73,7 +73,8 @@ export const unusedActorAttributesForAPI = [
required: false
},
{
model: AvatarModel,
model: ActorImageModel,
as: 'Avatar',
required: false
}
]
@ -100,7 +101,8 @@ export const unusedActorAttributesForAPI = [
required: false
},
{
model: AvatarModel,
model: ActorImageModel,
as: 'Avatar',
required: false
}
]
@ -213,18 +215,35 @@ export class ActorModel extends Model {
@UpdatedAt
updatedAt: Date
@ForeignKey(() => AvatarModel)
@ForeignKey(() => ActorImageModel)
@Column
avatarId: number
@BelongsTo(() => AvatarModel, {
@ForeignKey(() => ActorImageModel)
@Column
bannerId: number
@BelongsTo(() => ActorImageModel, {
foreignKey: {
name: 'avatarId',
allowNull: true
},
as: 'Avatar',
onDelete: 'set null',
hooks: true
})
Avatar: AvatarModel
Avatar: ActorImageModel
@BelongsTo(() => ActorImageModel, {
foreignKey: {
name: 'bannerId',
allowNull: true
},
as: 'Banner',
onDelete: 'set null',
hooks: true
})
Banner: ActorImageModel
@HasMany(() => ActorFollowModel, {
foreignKey: {
@ -496,7 +515,7 @@ export class ActorModel extends Model {
}
toFormattedSummaryJSON (this: MActorSummaryFormattable) {
let avatar: Avatar = null
let avatar: ActorImage = null
if (this.Avatar) {
avatar = this.Avatar.toFormattedJSON()
}

View File

@ -36,9 +36,9 @@ import {
MChannelSummaryFormattable
} from '../../types/models/video'
import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
import { ActorImageModel } from '../account/actor-image'
import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
import { ActorFollowModel } from '../activitypub/actor-follow'
import { AvatarModel } from '../avatar/avatar'
import { ServerModel } from '../server/server'
import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
@ -130,7 +130,8 @@ export type SummaryOptions = {
required: false
},
{
model: AvatarModel.unscoped(),
model: ActorImageModel.unscoped(),
as: 'Avatar',
required: false
}
]

View File

@ -490,12 +490,13 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build
'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"',
'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"',
'LEFT OUTER JOIN "avatar" AS "VideoChannel->Actor->Avatar" ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"',
'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' +
'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"',
'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' +
'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"',
'LEFT OUTER JOIN "avatar" AS "VideoChannel->Account->Actor->Avatar" ' +
'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' +
'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"',
'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"'

View File

@ -100,10 +100,10 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models
import { VideoAbuseModel } from '../abuse/video-abuse'
import { AccountModel } from '../account/account'
import { AccountVideoRateModel } from '../account/account-video-rate'
import { ActorImageModel } from '../account/actor-image'
import { UserModel } from '../account/user'
import { UserVideoHistoryModel } from '../account/user-video-history'
import { ActorModel } from '../activitypub/actor'
import { AvatarModel } from '../avatar/avatar'
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { ServerModel } from '../server/server'
import { TrackerModel } from '../server/tracker'
@ -286,7 +286,8 @@ export type AvailableForListIDsOptions = {
required: false
},
{
model: AvatarModel.unscoped(),
model: ActorImageModel.unscoped(),
as: 'Avatar',
required: false
}
]
@ -308,7 +309,8 @@ export type AvailableForListIDsOptions = {
required: false
},
{
model: AvatarModel.unscoped(),
model: ActorImageModel.unscoped(),
as: 'Avatar',
required: false
}
]
@ -1703,7 +1705,7 @@ export class VideoModel extends Model {
function buildActor (rowActor: any) {
const avatarModel = rowActor.Avatar.id !== null
? new AvatarModel(pick(rowActor.Avatar, avatarKeys), buildOpts)
? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts)
: null
const serverModel = rowActor.Server.id !== null

View File

@ -0,0 +1,12 @@
import { ActorImageModel } from '../../../models/account/actor-image'
import { FunctionProperties } from '@shared/core-utils'
export type MActorImage = ActorImageModel
// ############################################################################
// Format for API or AP object
export type MActorImageFormattable =
FunctionProperties<MActorImage> &
Pick<MActorImage, 'filename' | 'createdAt' | 'updatedAt'>

View File

@ -1,15 +1,15 @@
import { ActorModel } from '../../../models/activitypub/actor'
import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils'
import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account'
import { ActorModel } from '../../../models/activitypub/actor'
import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server'
import { MAvatar, MAvatarFormattable } from './avatar'
import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video'
import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account'
import { MActorImage, MActorImageFormattable } from './actor-image'
type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M>
// ############################################################################
export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server'>
export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server' | 'Banner'>
// ############################################################################
@ -34,7 +34,7 @@ export type MActorRedundancyAllowedOpt = PickWithOpt<ActorModel, 'Server', MServ
export type MActorDefaultLight =
MActorLight &
Use<'Server', MServerHost> &
Use<'Avatar', MAvatar>
Use<'Avatar', MActorImage>
export type MActorAccountId =
MActor &
@ -78,7 +78,7 @@ export type MActorServer =
export type MActorDefault =
MActor &
Use<'Server', MServer> &
Use<'Avatar', MAvatar>
Use<'Avatar', MActorImage>
// Actor with channel that is associated to an account and its actor
// Actor -> VideoChannel -> Account -> Actor
@ -89,7 +89,7 @@ export type MActorChannelAccountActor =
export type MActorFull =
MActor &
Use<'Server', MServer> &
Use<'Avatar', MAvatar> &
Use<'Avatar', MActorImage> &
Use<'Account', MAccount> &
Use<'VideoChannel', MChannelAccountActor>
@ -97,7 +97,7 @@ export type MActorFull =
export type MActorFullActor =
MActor &
Use<'Server', MServer> &
Use<'Avatar', MAvatar> &
Use<'Avatar', MActorImage> &
Use<'Account', MAccountDefault> &
Use<'VideoChannel', MChannelAccountDefault>
@ -109,7 +109,7 @@ export type MActorSummary =
FunctionProperties<MActor> &
Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> &
Use<'Server', MServerHost> &
Use<'Avatar', MAvatar>
Use<'Avatar', MActorImage>
export type MActorSummaryBlocks =
MActorSummary &
@ -127,7 +127,7 @@ export type MActorSummaryFormattable =
FunctionProperties<MActor> &
Pick<MActor, 'url' | 'preferredUsername'> &
Use<'Server', MServerHost> &
Use<'Avatar', MAvatarFormattable>
Use<'Avatar', MActorImageFormattable>
export type MActorFormattable =
MActorSummaryFormattable &
@ -136,4 +136,4 @@ export type MActorFormattable =
export type MActorAP =
MActor &
Use<'Avatar', MAvatar>
Use<'Avatar', MActorImage>

View File

@ -1,12 +0,0 @@
import { AvatarModel } from '../../../models/avatar/avatar'
import { FunctionProperties } from '@shared/core-utils'
export type MAvatar = AvatarModel
// ############################################################################
// Format for API or AP object
export type MAvatarFormattable =
FunctionProperties<MAvatar> &
Pick<MAvatar, 'filename' | 'createdAt' | 'updatedAt'>

View File

@ -1,5 +1,5 @@
export * from './account'
export * from './account-blocklist'
export * from './actor'
export * from './actor-follow'
export * from './avatar'
export * from './actor-image'
export * from './actor'

View File

@ -5,10 +5,10 @@ import { PluginModel } from '@server/models/server/plugin'
import { PickWith, PickWithOpt } from '@shared/core-utils'
import { AbuseModel } from '../../../models/abuse/abuse'
import { AccountModel } from '../../../models/account/account'
import { ActorImageModel } from '../../../models/account/actor-image'
import { UserNotificationModel } from '../../../models/account/user-notification'
import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { AvatarModel } from '../../../models/avatar/avatar'
import { ServerModel } from '../../../models/server/server'
import { VideoModel } from '../../../models/video/video'
import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
@ -29,7 +29,7 @@ export module UserNotificationIncludes {
export type ActorInclude =
Pick<ActorModel, 'preferredUsername' | 'getHost'> &
PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> &
PickWith<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>> &
PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'>
@ -75,7 +75,7 @@ export module UserNotificationIncludes {
Pick<ActorModel, 'preferredUsername' | 'getHost'> &
PickWith<ActorModel, 'Account', AccountInclude> &
PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> &
PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>>
PickWithOpt<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>>
export type ActorFollowing =
Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> &

View File

@ -1,5 +1,5 @@
import { ActorImage } from './actor-image.model'
import { Actor } from './actor.model'
import { Avatar } from '../avatars'
export interface Account extends Actor {
displayName: string
@ -14,5 +14,5 @@ export interface AccountSummary {
displayName: string
url: string
host: string
avatar?: Avatar
avatar?: ActorImage
}

View File

@ -1,4 +1,4 @@
export interface Avatar {
export interface ActorImage {
path: string
url?: string

View File

@ -0,0 +1,4 @@
export const enum ActorImageType {
AVATAR = 1,
BANNER = 2
}

View File

@ -1,4 +1,4 @@
import { Avatar } from '../avatars/avatar.model'
import { ActorImage } from './actor-image.model'
export interface Actor {
id: number
@ -9,5 +9,5 @@ export interface Actor {
followersCount: number
createdAt: Date | string
updatedAt: Date | string
avatar?: Avatar
avatar?: ActorImage
}

View File

@ -1,3 +1,5 @@
export * from './account.model'
export * from './actor-image.model'
export * from './actor-image.type'
export * from './actor.model'
export * from './follow.model'

View File

@ -1 +0,0 @@
export * from './avatar.model'

View File

@ -1,6 +1,5 @@
export * from './activitypub'
export * from './actors'
export * from './avatars'
export * from './moderation'
export * from './bulk'
export * from './redundancy'

View File

@ -1,6 +1,5 @@
import { Actor } from '../../actors/actor.model'
import { Account } from '../../actors/index'
import { Avatar } from '../../avatars'
import { Account, ActorImage } from '../../actors'
export type ViewsPerDate = {
date: Date
@ -24,5 +23,5 @@ export interface VideoChannelSummary {
displayName: string
url: string
host: string
avatar?: Avatar
avatar?: ActorImage
}