"jsonld": "~1.1.0",
"jsonld-signatures": "https://github.com/Chocobozzz/jsonld-signatures#rsa2017",
"lodash": "^4.17.10",
+ "lru-cache": "^5.1.1",
"magnet-uri": "^5.1.4",
"memoizee": "^0.4.14",
"morgan": "^1.5.3",
"@types/fs-extra": "^8.0.0",
"@types/libxmljs": "^0.18.0",
"@types/lodash": "^4.14.64",
+ "@types/lru-cache": "^5.1.0",
"@types/magnet-uri": "^5.1.1",
"@types/maildev": "^0.0.1",
"@types/memoizee": "^0.4.2",
clientsRouter,
feedsRouter,
staticRouter,
+ lazyStaticRouter,
servicesRouter,
pluginsRouter,
webfingerRouter,
// Static files
app.use('/', staticRouter)
+app.use('/', lazyStaticRouter)
// Client files, last valid routes!
if (cli.client) app.use('/', clientsRouter)
export * from './feeds'
export * from './services'
export * from './static'
+export * from './lazy-static'
export * from './webfinger'
export * from './tracker'
export * from './bots'
--- /dev/null
+import * as cors from 'cors'
+import * as express from 'express'
+import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
+import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
+import { asyncMiddleware } from '../middlewares'
+import { AvatarModel } from '../models/avatar/avatar'
+import { logger } from '../helpers/logger'
+import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar'
+
+const lazyStaticRouter = express.Router()
+
+lazyStaticRouter.use(cors())
+
+lazyStaticRouter.use(
+ LAZY_STATIC_PATHS.AVATARS + ':filename',
+ asyncMiddleware(getAvatar)
+)
+
+lazyStaticRouter.use(
+ LAZY_STATIC_PATHS.PREVIEWS + ':uuid.jpg',
+ asyncMiddleware(getPreview)
+)
+
+lazyStaticRouter.use(
+ LAZY_STATIC_PATHS.VIDEO_CAPTIONS + ':videoId-:captionLanguage([a-z]+).vtt',
+ asyncMiddleware(getVideoCaption)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ lazyStaticRouter,
+ getPreview,
+ getVideoCaption
+}
+
+// ---------------------------------------------------------------------------
+
+async function getAvatar (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 })
+ }
+
+ const avatar = await AvatarModel.loadByName(filename)
+ if (avatar.onDisk === false) {
+ if (!avatar.fileUrl) return res.sendStatus(404)
+
+ logger.info('Lazy serve remote avatar image %s.', avatar.fileUrl)
+
+ await pushAvatarProcessInQueue({ filename: avatar.filename, fileUrl: avatar.fileUrl })
+
+ avatar.onDisk = true
+ avatar.save()
+ .catch(err => logger.error('Cannot save new avatar disk state.', { err }))
+ }
+
+ const path = avatar.getPath()
+
+ avatarPathUnsafeCache.set(filename, path)
+ return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER })
+}
+
+async function getPreview (req: express.Request, res: express.Response) {
+ const result = await VideosPreviewCache.Instance.getFilePath(req.params.uuid)
+ if (!result) return res.sendStatus(404)
+
+ return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })
+}
+
+async function getVideoCaption (req: express.Request, res: express.Response) {
+ const result = await VideosCaptionCache.Instance.getFilePath({
+ videoId: req.params.videoId,
+ language: req.params.captionLanguage
+ })
+ if (!result) return res.sendStatus(404)
+
+ return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })
+}
STATIC_PATHS,
WEBSERVER
} from '../initializers/constants'
-import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
import { cacheRoute } from '../middlewares/cache'
import { asyncMiddleware, videosGetValidator } from '../middlewares'
import { VideoModel } from '../models/video/video'
import { join } from 'path'
import { root } from '../helpers/core-utils'
import { CONFIG } from '../initializers/config'
+import { getPreview, getVideoCaption } from './lazy-static'
const staticRouter = express.Router()
express.static(thumbnailsPhysicalPath, { maxAge: STATIC_MAX_AGE.SERVER, fallthrough: false }) // 404 if the file does not exist
)
+// DEPRECATED: use lazy-static route instead
const avatarsPhysicalPath = CONFIG.STORAGE.AVATARS_DIR
staticRouter.use(
STATIC_PATHS.AVATARS,
express.static(avatarsPhysicalPath, { maxAge: STATIC_MAX_AGE.SERVER, fallthrough: false }) // 404 if the file does not exist
)
-// We don't have video previews, fetch them from the origin instance
+// DEPRECATED: use lazy-static route instead
staticRouter.use(
STATIC_PATHS.PREVIEWS + ':uuid.jpg',
asyncMiddleware(getPreview)
)
-// We don't have video captions, fetch them from the origin instance
+// DEPRECATED: use lazy-static route instead
staticRouter.use(
STATIC_PATHS.VIDEO_CAPTIONS + ':videoId-:captionLanguage([a-z]+).vtt',
asyncMiddleware(getVideoCaption)
// ---------------------------------------------------------------------------
-async function getPreview (req: express.Request, res: express.Response) {
- const result = await VideosPreviewCache.Instance.getFilePath(req.params.uuid)
- if (!result) return res.sendStatus(404)
-
- return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
-}
-
-async function getVideoCaption (req: express.Request, res: express.Response) {
- const result = await VideosCaptionCache.Instance.getFilePath({
- videoId: req.params.videoId,
- language: req.params.captionLanguage
- })
- if (!result) return res.sendStatus(404)
-
- return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
-}
-
async function generateNodeinfo (req: express.Request, res: express.Response) {
const { totalVideos } = await VideoModel.getStats()
const { totalLocalVideoComments } = await VideoCommentModel.getStats()
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 415
+const LAST_MIGRATION_VERSION = 420
// ---------------------------------------------------------------------------
TORRENTS: '/download/torrents/',
VIDEOS: '/download/videos/'
}
+const LAZY_STATIC_PATHS = {
+ AVATARS: '/lazy-static/avatars/',
+ PREVIEWS: '/static/previews/',
+ VIDEO_CAPTIONS: '/static/video-captions/'
+}
// Cache control
let STATIC_MAX_AGE = {
}
}
-const CACHE = {
+const LRU_CACHE = {
USER_TOKENS: {
- MAX_SIZE: 10000
+ MAX_SIZE: 1000
+ },
+ AVATAR_STATIC: {
+ MAX_SIZE: 500
}
}
OVERVIEWS_SAMPLE: 1000 * 3600 * 4 // 4 hours
}
+const QUEUE_CONCURRENCY = {
+ AVATAR_PROCESS_IMAGE: 3
+}
+
const REDUNDANCY = {
VIDEOS: {
RANDOMIZED_FACTOR: 5
WEBSERVER,
API_VERSION,
PEERTUBE_VERSION,
+ LAZY_STATIC_PATHS,
HLS_REDUNDANCY_DIRECTORY,
P2P_MEDIA_LOADER_PEER_VERSION,
AVATARS_SIZE,
VIDEO_PRIVACIES,
VIDEO_LICENCES,
VIDEO_STATES,
+ QUEUE_CONCURRENCY,
VIDEO_RATE_TYPES,
VIDEO_TRANSCODING_FPS,
FFMPEG_NICE,
VIDEO_ABUSE_STATES,
- CACHE,
+ LRU_CACHE,
JOB_REQUEST_TIMEOUT,
USER_PASSWORD_RESET_LIFETIME,
MEMOIZE_TTL,
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction,
+ queryInterface: Sequelize.QueryInterface,
+ sequelize: Sequelize.Sequelize,
+ db: any
+}): Promise<void> {
+ {
+ // We'll add a unique index on filename, so delete duplicates or PeerTube won't start
+ const query = 'DELETE FROM "avatar" s1 ' +
+ 'USING (SELECT MIN(id) as id, filename FROM "avatar" GROUP BY "filename" HAVING COUNT(*) > 1) s2 ' +
+ 'WHERE s1."filename" = s2."filename" AND s1.id <> s2.id'
+ await utils.sequelize.query(query)
+ }
+
+ {
+ const data = {
+ type: Sequelize.STRING,
+ allowNull: true,
+ defaultValue: null
+ }
+
+ await utils.queryInterface.addColumn('avatar', 'fileUrl', data)
+ }
+
+ {
+ const data = {
+ type: Sequelize.BOOLEAN,
+ allowNull: true,
+ defaultValue: null
+ }
+
+ await utils.queryInterface.addColumn('avatar', 'onDisk', data)
+ }
+
+ {
+ const query = 'UPDATE "avatar" SET "onDisk" = true;'
+ await utils.sequelize.query(query)
+ }
+
+ {
+ const data = {
+ type: Sequelize.BOOLEAN,
+ allowNull: false,
+ defaultValue: null
+ }
+
+ await utils.queryInterface.changeColumn('avatar', 'onDisk', data)
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger'
import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
-import { doRequest, downloadImage } from '../../helpers/requests'
+import { doRequest } from '../../helpers/requests'
import { getUrlFromWebfinger } from '../../helpers/webfinger'
-import { AVATARS_SIZE, MIMETYPES, WEBSERVER } from '../../initializers/constants'
+import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
import { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor'
import { AvatarModel } from '../../models/avatar/avatar'
import { JobQueue } from '../job-queue'
import { getServerActor } from '../../helpers/utils'
import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
-import { CONFIG } from '../../initializers/config'
import { sequelizeTypescript } from '../../initializers/database'
// Set account keys, this could be long so process after the account creation and do not block the client
actorInstance.followingUrl = attributes.following
}
-async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
- if (avatarName !== undefined) {
- if (actorInstance.avatarId) {
+async function updateActorAvatarInstance (actor: ActorModel, info: { name: string, onDisk: boolean, fileUrl: string }, t: Transaction) {
+ if (info.name !== undefined) {
+ if (actor.avatarId) {
try {
- await actorInstance.Avatar.destroy({ transaction: t })
+ await actor.Avatar.destroy({ transaction: t })
} catch (err) {
- logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
+ logger.error('Cannot remove old avatar of actor %s.', actor.url, { err })
}
}
const avatar = await AvatarModel.create({
- filename: avatarName
+ filename: info.name,
+ onDisk: info.onDisk,
+ fileUrl: info.fileUrl
}, { transaction: t })
- actorInstance.set('avatarId', avatar.id)
- actorInstance.Avatar = avatar
+ actor.avatarId = avatar.id
+ actor.Avatar = avatar
}
- return actorInstance
+ return actor
}
async function fetchActorTotalItems (url: string) {
}
}
-async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
+async function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
if (
actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
isActivityPubUrlValid(actorJSON.icon.url)
) {
const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType]
- const avatarName = uuidv4() + extension
- await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE)
-
- return avatarName
+ return {
+ name: uuidv4() + extension,
+ fileUrl: actorJSON.icon.url
+ }
}
return undefined
return sequelizeTypescript.transaction(async t => {
updateInstanceWithAnother(actor, result.actor)
- if (result.avatarName !== undefined) {
- await updateActorAvatarInstance(actor, result.avatarName, t)
+ if (result.avatar !== undefined) {
+ const avatarInfo = {
+ name: result.avatar.name,
+ fileUrl: result.avatar.fileUrl,
+ onDisk: false
+ }
+
+ await updateActorAvatarInstance(actor, avatarInfo, t)
}
// Force update
buildActorInstance,
setAsyncActorKeys,
fetchActorTotalItems,
- fetchAvatarIfExists,
+ getAvatarInfoIfExists,
updateActorInstance,
refreshActorIfNeeded,
updateActorAvatarInstance,
const [ server ] = await ServerModel.findOrCreate(serverOptions)
// Save our new account in database
- actor.set('serverId', server.id)
+ actor.serverId = server.id
// Avatar?
- if (result.avatarName) {
+ if (result.avatar) {
const avatar = await AvatarModel.create({
- filename: result.avatarName
+ filename: result.avatar.name,
+ fileUrl: result.avatar.fileUrl,
+ onDisk: false
}, { transaction: t })
- actor.set('avatarId', avatar.id)
+
+ actor.avatarId = avatar.id
}
// Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
summary: string
support?: string
playlists?: string
- avatarName?: string
+ avatar?: {
+ name: string,
+ fileUrl: string
+ }
attributedTo: ActivityPubAttributedTo[]
}
async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
followingUrl: actorJSON.following
})
- const avatarName = await fetchAvatarIfExists(actorJSON)
+ const avatarInfo = await getAvatarInfoIfExists(actorJSON)
const name = actorJSON.name || actorJSON.preferredUsername
return {
result: {
actor,
name,
- avatarName,
+ avatar: avatarInfo,
summary: actorJSON.summary,
support: actorJSON.support,
playlists: actorJSON.playlists,
import { AccountModel } from '../../../models/account/account'
import { ActorModel } from '../../../models/activitypub/actor'
import { VideoChannelModel } from '../../../models/video/video-channel'
-import { fetchAvatarIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor'
+import { getAvatarInfoIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor'
import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
let accountOrChannelFieldsSave: object
// Fetch icon?
- const avatarName = await fetchAvatarIfExists(actorAttributesToUpdate)
+ const avatarInfo = await getAvatarInfoIfExists(actorAttributesToUpdate)
try {
await sequelizeTypescript.transaction(async t => {
await updateActorInstance(actor, actorAttributesToUpdate)
- if (avatarName !== undefined) {
- await updateActorAvatarInstance(actor, avatarName, t)
+ if (avatarInfo !== undefined) {
+ const avatarOptions = Object.assign({}, avatarInfo, { onDisk: false })
+
+ await updateActorAvatarInstance(actor, avatarOptions, t)
}
await actor.save({ transaction: t })
import 'multer'
import { sendUpdateActor } from './activitypub/send'
-import { AVATARS_SIZE } from '../initializers/constants'
+import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
import { updateActorAvatarInstance } from './activitypub'
import { processImage } from '../helpers/image-utils'
import { AccountModel } from '../models/account/account'
import * as uuidv4 from 'uuid/v4'
import { CONFIG } from '../initializers/config'
import { sequelizeTypescript } from '../initializers/database'
+import * as LRUCache from 'lru-cache'
+import { queue } from 'async'
+import { downloadImage } from '../helpers/requests'
async function updateActorAvatarFile (avatarPhysicalFile: Express.Multer.File, accountOrChannel: AccountModel | VideoChannelModel) {
const extension = extname(avatarPhysicalFile.filename)
return retryTransactionWrapper(() => {
return sequelizeTypescript.transaction(async t => {
- const updatedActor = await updateActorAvatarInstance(accountOrChannel.Actor, avatarName, t)
+ const avatarInfo = {
+ name: avatarName,
+ fileUrl: null,
+ onDisk: true
+ }
+
+ const updatedActor = await updateActorAvatarInstance(accountOrChannel.Actor, avatarInfo, t)
await updatedActor.save({ transaction: t })
await sendUpdateActor(accountOrChannel, t)
})
}
+type DownloadImageQueueTask = { fileUrl: string, filename: string }
+
+const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
+ downloadImage(task.fileUrl, CONFIG.STORAGE.AVATARS_DIR, task.filename, AVATARS_SIZE)
+ .then(() => cb())
+ .catch(err => cb(err))
+}, QUEUE_CONCURRENCY.AVATAR_PROCESS_IMAGE)
+
+function pushAvatarProcessInQueue (task: DownloadImageQueueTask) {
+ return new Promise((res, rej) => {
+ downloadImageQueue.push(task, err => {
+ if (err) return rej(err)
+
+ return res()
+ })
+ })
+}
+
+// Unsafe so could returns paths that does not exist anymore
+const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVATAR_STATIC.MAX_SIZE })
+
export {
- updateActorAvatarFile
+ avatarPathUnsafeCache,
+ updateActorAvatarFile,
+ pushAvatarProcessInQueue
}
import { UserModel } from '../models/account/user'
import { OAuthClientModel } from '../models/oauth/oauth-client'
import { OAuthTokenModel } from '../models/oauth/oauth-token'
-import { CACHE } from '../initializers/constants'
+import { LRU_CACHE } from '../initializers/constants'
import { Transaction } from 'sequelize'
import { CONFIG } from '../initializers/config'
+import * as LRUCache from 'lru-cache'
type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
-let accessTokenCache: { [ accessToken: string ]: OAuthTokenModel } = {}
-let userHavingToken: { [ userId: number ]: string } = {}
+
+const accessTokenCache = new LRUCache<string, OAuthTokenModel>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
+const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
// ---------------------------------------------------------------------------
}
function clearCacheByUserId (userId: number) {
- const token = userHavingToken[userId]
+ const token = userHavingToken.get(userId)
+
if (token !== undefined) {
- accessTokenCache[ token ] = undefined
- userHavingToken[ userId ] = undefined
+ accessTokenCache.del(token)
+ userHavingToken.del(userId)
}
}
function clearCacheByToken (token: string) {
- const tokenModel = accessTokenCache[ token ]
+ const tokenModel = accessTokenCache.get(token)
+
if (tokenModel !== undefined) {
- userHavingToken[tokenModel.userId] = undefined
- accessTokenCache[ token ] = undefined
+ userHavingToken.del(tokenModel.userId)
+ accessTokenCache.del(token)
}
}
if (!bearerToken) return Bluebird.resolve(undefined)
- if (accessTokenCache[bearerToken] !== undefined) return Bluebird.resolve(accessTokenCache[bearerToken])
+ if (accessTokenCache.has(bearerToken)) return Bluebird.resolve(accessTokenCache.get(bearerToken))
return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
.then(tokenModel => {
if (tokenModel) {
- // Reinit our cache
- if (Object.keys(accessTokenCache).length > CACHE.USER_TOKENS.MAX_SIZE) {
- accessTokenCache = {}
- userHavingToken = {}
- }
-
- accessTokenCache[ bearerToken ] = tokenModel
- userHavingToken[ tokenModel.userId ] = tokenModel.accessToken
+ accessTokenCache.set(bearerToken, tokenModel)
+ userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
}
return tokenModel
id: this.ActorFollow.ActorFollower.Account.id,
displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
name: this.ActorFollow.ActorFollower.preferredUsername,
- avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined,
+ avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined,
host: this.ActorFollow.ActorFollower.getHost()
},
following: {
private formatActor (accountOrChannel: AccountModel | VideoChannelModel) {
const avatar = accountOrChannel.Actor.Avatar
- ? { path: accountOrChannel.Actor.Avatar.getWebserverPath() }
+ ? { path: accountOrChannel.Actor.Avatar.getStaticPath() }
: undefined
return {
getAvatarUrl () {
if (!this.avatarId) return undefined
- return WEBSERVER.URL + this.Avatar.getWebserverPath()
+ return WEBSERVER.URL + this.Avatar.getStaticPath()
}
isOutdated () {
import { join } from 'path'
-import { AfterDestroy, AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Avatar } from '../../../shared/models/avatars/avatar.model'
-import { STATIC_PATHS } from '../../initializers/constants'
+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 { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@Table({
- tableName: 'avatar'
+ tableName: 'avatar',
+ indexes: [
+ {
+ fields: [ 'filename' ],
+ unique: true
+ }
+ ]
})
export class AvatarModel extends Model<AvatarModel> {
@Column
filename: string
+ @AllowNull(true)
+ @Is('AvatarFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl'))
+ @Column
+ fileUrl: string
+
+ @AllowNull(false)
+ @Column
+ onDisk: boolean
+
@CreatedAt
createdAt: Date
.catch(err => logger.error('Cannot remove avatar file %s.', instance.filename, err))
}
+ static loadByName (filename: string) {
+ const query = {
+ where: {
+ filename
+ }
+ }
+
+ return AvatarModel.findOne(query)
+ }
+
toFormattedJSON (): Avatar {
return {
- path: this.getWebserverPath(),
+ path: this.getStaticPath(),
createdAt: this.createdAt,
updatedAt: this.updatedAt
}
}
- getWebserverPath () {
- return join(STATIC_PATHS.AVATARS, this.filename)
+ getStaticPath () {
+ return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
+ }
+
+ getPath () {
+ return join(CONFIG.STORAGE.AVATARS_DIR, this.filename)
}
removeAvatar () {
import { join } from 'path'
import { AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
-import { STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
+import { LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
import { logger } from '../../helpers/logger'
import { remove } from 'fs-extra'
import { CONFIG } from '../../initializers/config'
[ThumbnailType.PREVIEW]: {
label: 'preview',
directory: CONFIG.STORAGE.PREVIEWS_DIR,
- staticPath: STATIC_PATHS.PREVIEWS
+ staticPath: LAZY_STATIC_PATHS.PREVIEWS
}
}
import { VideoModel } from './video'
import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
-import { STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers/constants'
+import { LAZY_STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers/constants'
import { join } from 'path'
import { logger } from '../../helpers/logger'
import { remove } from 'fs-extra'
}
getCaptionStaticPath () {
- return join(STATIC_PATHS.VIDEO_CAPTIONS, this.getCaptionName())
+ return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.getCaptionName())
}
getCaptionName () {
CONSTRAINTS_FIELDS,
HLS_REDUNDANCY_DIRECTORY,
HLS_STREAMING_PLAYLIST_DIRECTORY,
+ LAZY_STATIC_PATHS,
REMOTE_SCHEME,
STATIC_DOWNLOAD_PATHS,
STATIC_PATHS,
if (!preview) return null
// We use a local cache, so specify our cache endpoint instead of potential remote URL
- return join(STATIC_PATHS.PREVIEWS, preview.filename)
+ return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename)
}
toFormattedJSON (options?: VideoFormattingJSONOptions): Video {
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.136.tgz#413e85089046b865d960c9ff1d400e04c31ab60f"
integrity sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA==
+"@types/lru-cache@^5.1.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03"
+ integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==
+
"@types/magnet-uri@*", "@types/magnet-uri@^5.1.1":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/magnet-uri/-/magnet-uri-5.1.2.tgz#7860417399d52ddc0be1021d570b4ac93ffc133e"
pseudomap "^1.0.2"
yallist "^2.1.2"
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
lru-queue@0.1:
version "0.1.0"
resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
-yallist@^3.0.0, yallist@^3.0.3:
+yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==