UNPKG

@sync-in/server

Version:

The secure, open-source platform for file storage, sharing, collaboration, and sync

824 lines (823 loc) 44 kB
/* * Copyright (C) 2012-2025 Johan Legrand <johan.legrand@sync-in.com> * This file is part of Sync-in | The open source file sync and share solution * See the LICENSE file for licensing details */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "SpacesManager", { enumerable: true, get: function() { return SpacesManager; } }); const _common = require("@nestjs/common"); const _drizzleorm = require("drizzle-orm"); const _promises = /*#__PURE__*/ _interop_require_default(require("node:fs/promises")); const _nodepath = /*#__PURE__*/ _interop_require_default(require("node:path")); const _constants = require("../../../common/constants"); const _functions = require("../../../common/functions"); const _shared = require("../../../common/shared"); const _configenvironment = require("../../../configuration/config.environment"); const _contextmanagerservice = require("../../../infrastructure/context/services/context-manager.service"); const _fileerror = require("../../files/models/file-error"); const _files = require("../../files/utils/files"); const _links = require("../../links/constants/links"); const _notifications = require("../../notifications/constants/notifications"); const _notificationsmanagerservice = require("../../notifications/services/notifications-manager.service"); const _sharesmanagerservice = require("../../shares/services/shares-manager.service"); const _member = require("../../users/constants/member"); const _user = require("../../users/constants/user"); const _usermodel = require("../../users/models/user.model"); const _usersschema = require("../../users/schemas/users.schema"); const _usersqueriesservice = require("../../users/services/users-queries.service"); const _cache = require("../constants/cache"); const _spaces = require("../constants/spaces"); const _spaceenvmodel = require("../models/space-env.model"); const _spacepropsmodel = require("../models/space-props.model"); const _spacemodel = require("../models/space.model"); const _spacesschema = require("../schemas/spaces.schema"); const _paths = require("../utils/paths"); const _permissions = require("../utils/permissions"); const _spacesqueriesservice = require("./spaces-queries.service"); function _interop_require_default(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _ts_decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function _ts_metadata(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); } let SpacesManager = class SpacesManager { listSpaces(userId) { return this.spacesQueries.spaces(userId); } spacesWithDetails(userId) { return this.spacesQueries.spacesWithDetails(userId); } uniqueRootName(name, names) { if (names.find((fName)=>name.toLowerCase() === fName.toLowerCase())) { const nameExtension = _nodepath.default.extname(name); const nameWithoutExtension = _nodepath.default.basename(name, nameExtension); const originalName = nameWithoutExtension.replace(_shared.regExpNumberSuffix, ''); let count = 1; let newName = `${originalName}-${count}${nameExtension}`; while(names.find((fName)=>newName.toLowerCase() === fName.toLowerCase())){ count += 1; newName = `${originalName}-${count}${nameExtension}`; } return newName; } return null; } async spaceEnv(user, urlSegments, skipEndpointProtection = false) { /* SpaceEnv builder */ let [repository, spaceAlias, rootAlias, ...paths] = urlSegments; if (!repository || !spaceAlias && repository !== _spaces.SPACE_REPOSITORY.SHARES || Object.values(_spaces.SPACE_REPOSITORY).indexOf(repository) === -1) { throw new Error(`Space path is not valid : ${urlSegments}`); } let space; if (spaceAlias === _spaces.SPACE_ALIAS.PERSONAL) { /* Personal Space (static) */ if (rootAlias) { // there is no root in a personal space paths.unshift(rootAlias); rootAlias = null; } space = new _spaceenvmodel.SpaceEnv(_spaces.SPACE_PERSONAL, rootAlias, false); } else if (repository === _spaces.SPACE_REPOSITORY.SHARES) { if (spaceAlias) { /* Share */ const spacePermissions = await this.sharesManager.permissions(user, spaceAlias); if (spacePermissions) { space = new _spaceenvmodel.SpaceEnv(spacePermissions, null, false); } } else { /* Shares List (static) */ space = new _spaceenvmodel.SpaceEnv(_spaces.SPACE_SHARES, null, false); } } else { /* Space */ if (repository === _spaces.SPACE_REPOSITORY.TRASH && rootAlias) { // there is no root in a trash space paths.unshift(rootAlias); rootAlias = null; } const spacePermissions = await this.spacesQueries.permissions(user.id, spaceAlias, rootAlias); if (spacePermissions) { space = new _spaceenvmodel.SpaceEnv(spacePermissions, rootAlias || ''); } } if (!space) return null; try { space.setup(user, repository, rootAlias, paths, urlSegments, skipEndpointProtection); await this.setQuotaExceeded(user, space); return space; } catch (e) { this.logger.warn(`${this.spaceEnv.name} - *${space.alias}* : ${e}`); throw new _common.HttpException(e.message, e instanceof _fileerror.FileError ? e.httpCode : _common.HttpStatus.BAD_REQUEST); } } async searchSpaces(userId, searchSpaceDto) { const sps = []; for (const s of (await this.spacesQueries.spaces(userId, true))){ if (searchSpaceDto.shareInsidePermission && !(0, _permissions.haveSpacePermission)(s, _spaces.SPACE_OPERATION.SHARE_INSIDE)) { continue; } if (!searchSpaceDto.search || `${s.name} ${s.alias} ${s.description || ''}`.toLowerCase().indexOf(searchSpaceDto.search) > -1) { sps.push(s); } if (sps.length >= searchSpaceDto.limit) { break; } } return sps; } async listSpacesWithPermissions(user) { return Promise.all((await this.spacesQueries.spaces(user.id, true)).map(async (s)=>{ const space = new _spaceenvmodel.SpaceEnv(s); await this.setQuotaExceeded(user, space); return space; })); } async listTrashes(user) { const trashes = []; // todo: store 'Personal files' as const somewhere (used in frontend too) const personalTrash = { id: 0, name: 'Personal files', alias: _spaces.SPACE_ALIAS.PERSONAL, nb: 0, mtime: 0, ctime: 0 }; for (const space of [ ...await this.listSpaces(user.id), personalTrash ]){ const rPath = space.alias === _spaces.SPACE_ALIAS.PERSONAL ? user.trashPath : _spacemodel.SpaceModel.getTrashPath(space.alias); try { space.nb = (await _promises.default.readdir(rPath)).filter((f)=>_configenvironment.configuration.applications.files.showHiddenFiles || f[0] !== '.').length; if (space.nb) { const stats = await _promises.default.stat(rPath); space.mtime = stats.mtime.getTime(); space.ctime = stats.birthtime.getTime(); trashes.push(space); } } catch (e) { this.logger.error(`${this.listTrashes.name} - ${e}`); } } return trashes; } async getSpace(user, spaceId) { const space = await this.userCanAccessSpace(user.id, spaceId, true); if (space.roots?.length && !user.isAdmin) { // remove external path if the current user is not an administrator for (const root of space.roots){ root.externalPath = null; } } return space; } async createSpace(user, createOrUpdateSpaceDto) { /* only users with admin space role can create a space */ // create space const space = new _spacepropsmodel.SpaceProps({ name: createOrUpdateSpaceDto.name, alias: await this.uniqueSpaceAlias(createOrUpdateSpaceDto.name, true), description: createOrUpdateSpaceDto.description, enabled: createOrUpdateSpaceDto.enabled, storageQuota: createOrUpdateSpaceDto.storageQuota, storageIndexing: createOrUpdateSpaceDto.storageIndexing, disabledAt: createOrUpdateSpaceDto.enabled ? null : new Date() }); try { space.id = await this.spacesQueries.createSpace(space); } catch (e) { this.logger.error(`${this.createSpace.name} - unable to create space *${space.alias}* : ${e}`); throw new _common.HttpException('Unable to create space', e); } // create space paths await _spacemodel.SpaceModel.makePaths(space.alias); // add roots await this.updateRoots(user, space, space.roots, createOrUpdateSpaceDto.roots, [], []); // add members await this.updateMembers(user, space, createOrUpdateSpaceDto.members.concat(createOrUpdateSpaceDto.managers)); // create links after members, user must be a space manager to create links await this.sharesManager.createOrUpdateLinksAsMembers(user, space, _links.LINK_TYPE.SPACE, createOrUpdateSpaceDto.links); return this.spacesQueries.getSpaceAsManager(user.id, space.id); } async updateSpace(user, spaceId, createOrUpdateSpaceDto) { /* only managers of the space can update it */ const space = await this.userCanAccessSpace(user.id, spaceId, true); // check and update space info const spaceDiffProps = { modifiedAt: new Date() }; const props = [ 'name', 'description', 'enabled', 'storageQuota', 'storageIndexing' ]; for (const prop of props){ if (createOrUpdateSpaceDto[prop] !== space[prop]) { spaceDiffProps[prop] = createOrUpdateSpaceDto[prop]; if (prop === 'name') { spaceDiffProps.alias = await this.uniqueSpaceAlias(spaceDiffProps.name, true); if (space.alias !== spaceDiffProps.alias) { // must move the space to match the new alias const spaceLocationWasRenamed = await this.renameSpaceLocation(space.alias, spaceDiffProps.alias); if (!spaceLocationWasRenamed) { throw new _common.HttpException('Unable to rename space', _common.HttpStatus.INTERNAL_SERVER_ERROR); } space.alias = spaceDiffProps.alias; } } else if (prop === 'enabled') { spaceDiffProps.disabledAt = spaceDiffProps[prop] ? null : new Date(); } } } // updates in db this.spacesQueries.updateSpace(spaceId, spaceDiffProps).catch((e)=>this.logger.error(`${this.updateSpace.name} - ${e}`)); // checks & updates members const linkMembers = await this.sharesManager.createOrUpdateLinksAsMembers(user, space, _links.LINK_TYPE.SPACE, createOrUpdateSpaceDto.links); const rootOwnerIds = await this.updateMembers(user, space, [ ...createOrUpdateSpaceDto.members, ...createOrUpdateSpaceDto.managers, ...linkMembers ]); if (rootOwnerIds.length) { // removes the roots of removed members or those no longer having the `share inside` permission createOrUpdateSpaceDto.roots = createOrUpdateSpaceDto.roots.filter((r)=>rootOwnerIds.indexOf(r.owner.id) === -1); } // checks & updates roots const aliases = space.roots.map((r)=>r.alias); const names = [ ...await (0, _files.dirListFileNames)(_spacemodel.SpaceModel.getFilesPath(space.alias)), ...space.roots.map((r)=>r.name) ]; await this.updateRoots(user, space, space.roots, createOrUpdateSpaceDto.roots, aliases, names); if (rootOwnerIds.indexOf(user.id) > -1) { // current manager was removed return null; } else { return this.spacesQueries.getSpaceAsManager(user.id, spaceId); } } async deleteSpace(user, spaceId, deleteSpaceDto) { /* only managers of the space can disable it */ const space = await this.userCanAccessSpace(user.id, spaceId, true); // only admin can delete the space data, managers can only disable the space for 30 days const deleteNow = user.isAdmin && !!deleteSpaceDto?.deleteNow; try { if (deleteNow) { await this.sharesManager.deleteAllLinkMembers(spaceId, _links.LINK_TYPE.SPACE); } await this.deleteOrDisableSpace(space, deleteNow); this.logger.log(`${this.deleteSpace.name} - *${space.alias}* (${space.id}) was ${deleteNow ? 'deleted' : 'disabled'}`); } catch (e) { this.logger.error(`${this.deleteSpace.name} - *${space.alias}* (${space.id}) was not ${deleteNow ? 'deleted' : 'disabled'} : ${e}`); throw new _common.HttpException('Unable to delete space', _common.HttpStatus.INTERNAL_SERVER_ERROR); } } async getUserRoots(user, spaceId) { /* if user has no permissions on the space an empty array will be returned */ return this.spacesQueries.getSpaceRoots(spaceId, user.id); } async updateUserRoots(user, spaceId, userRoots, addOnly = false) { const space = await this.userCanAccessSpace(user.id, spaceId); if (space.role !== _spaces.SPACE_ROLE.IS_MANAGER && !(0, _permissions.haveSpacePermission)(space, _spaces.SPACE_OPERATION.SHARE_INSIDE)) { this.logger.warn(`is not allowed to share inside on this space : *${space.alias}* (${space.id})`); throw new _common.HttpException('You are not allowed to do this action', _common.HttpStatus.FORBIDDEN); } // current states const spaceRoots = await this.spacesQueries.getSpaceRoots(spaceId); const aliases = spaceRoots.map((r)=>r.alias); const names = [ ...await (0, _files.dirListFileNames)(_spacemodel.SpaceModel.getFilesPath(space.alias)), ...spaceRoots.map((r)=>r.name) ]; // force owner.id on new user roots (owner is optional and required for the next steps) userRoots.forEach((r)=>r.owner = { id: user.id }); if (addOnly) { // short circuit the `updateRoots` function // we need to provide all space roots to avoid collisions on aliases and names for new user roots const toAdd = await this.validateNewRoots(user, space, spaceRoots, userRoots, aliases, names); const status = await this.spacesQueries.updateSpaceRoots(user.id, space.id, toAdd, [], []); Object.entries(status).forEach(([action, roots])=>this.clearCachePermissionsAndOrNotify(space, action, user, null, roots).catch((e)=>this.logger.error(`${this.updateUserRoots.name} - ${e}`))); return this.getUserRoots(user, spaceId); } else { const currentUserRoots = spaceRoots.filter((r)=>r.owner?.id === user.id); await this.updateRoots(user, space, currentUserRoots, userRoots, aliases, names); return this.spacesQueries.getSpaceRoots(spaceId, user.id); } } async uniqueRootAlias(spaceId, alias, aliasesAndNames, replaceCount = false) { /* for some webdav clients the root alias is displayed instead of the file name. * This is why a root alias must be unique for files too */ if (aliasesAndNames.find((fName)=>alias.toLowerCase() === fName.toLowerCase())) { const aliasExtension = _nodepath.default.extname(alias); const aliasWithoutExtension = _nodepath.default.basename(alias, aliasExtension); const originalAlias = (0, _shared.createSlug)(aliasWithoutExtension, replaceCount); let count = 1; let newAlias = `${originalAlias}-${count}${aliasExtension}`; while(await this.spacesQueries.spaceRootExistsForAlias(spaceId, newAlias)){ count += 1; newAlias = `${originalAlias}-${count}${aliasExtension}`; } return newAlias; } return null; } async updatePersonalSpacesQuota(forUser) { const props = [ 'id', 'login', 'storageUsage', 'storageQuota' ]; for (const user of (await this.usersQueries.selectUsers(props, [ (0, _drizzleorm.lte)(_usersschema.users.role, _user.USER_ROLE.USER), ...forUser ? [ (0, _drizzleorm.eq)(_usersschema.users.id, forUser.id) ] : [] ]))){ const userPath = _usermodel.UserModel.getHomePath(user.login); if (!await (0, _files.isPathExists)(userPath)) { this.logger.warn(`${this.updatePersonalSpacesQuota.name} - *${user.login}* home path does not exist`); continue; } const [size, errors] = await (0, _files.dirSize)(userPath); for (const [path, error] of Object.entries(errors)){ this.logger.warn(`${this.updatePersonalSpacesQuota.name} - unable to get size for *${user.login}* on ${path} : ${error}`); } const spaceQuota = { storageUsage: size, storageQuota: user.storageQuota }; this.spacesQueries.cache.set(`${_cache.CACHE_QUOTA_USER_PREFIX}-${user.id}`, spaceQuota, _cache.CACHE_QUOTA_TTL).catch((e)=>this.logger.error(`${this.updatePersonalSpacesQuota.name} - user *${user.login}* (${user.id}) : ${e}`)); if (user.storageUsage !== spaceQuota.storageUsage) { this.usersQueries.updateUserOrGuest(user.id, { storageUsage: spaceQuota.storageUsage }).then((updated)=>updated && this.logger.log(`${this.updatePersonalSpacesQuota.name} - user *${user.login}* (${user.id}) - storage usage updated: ${spaceQuota.storageUsage}`)); } if (forUser) { return spaceQuota; } } } async updateSpacesQuota(spaceId) { for (const space of (await this.spacesQueries.spacesQuotaPaths(spaceId))){ const spacePath = _spacemodel.SpaceModel.getHomePath(space.alias); if (!await (0, _files.isPathExists)(spacePath)) { this.logger.warn(`${this.updateSpacesQuota.name} - *${space.alias}* home path does not exist`); continue; } let size = 0; for (const rPath of [ spacePath, ...space.externalPaths.filter(Boolean) ]){ const [rPathSize, errors] = await (0, _files.dirSize)(rPath); size += rPathSize; for (const [path, error] of Object.entries(errors)){ this.logger.warn(`${this.updateSpacesQuota.name} - unable to get size for *${space.alias}* on ${path} : ${error}`); } } const spaceQuota = { storageUsage: size, storageQuota: space.storageQuota }; this.spacesQueries.cache.set(`${_cache.CACHE_QUOTA_SPACE_PREFIX}-${space.id}`, spaceQuota, _cache.CACHE_QUOTA_TTL).catch((e)=>this.logger.error(`${this.updateSpacesQuota.name} - space *${space.alias}* (${space.id}) : ${e}`)); if (space.storageUsage !== spaceQuota.storageUsage) { this.spacesQueries.updateSpace(space.id, { storageUsage: spaceQuota.storageUsage }).then((updated)=>updated && this.logger.log(`${this.updateSpacesQuota.name} - space *${space.alias}* (${space.id}) - storage usage updated : ${spaceQuota.storageUsage}`)); } if (spaceId) { return spaceQuota; } } } async deleteExpiredSpaces() { /* Removes spaces that have been disabled for more than 30 days */ const props = [ 'id', 'name', 'alias', 'disabledAt' ]; for (const space of (await this.spacesQueries.selectSpaces(props, [ (0, _drizzleorm.eq)(_spacesschema.spaces.enabled, false), (0, _drizzleorm.isNotNull)(_spacesschema.spaces.disabledAt) ]))){ const disabled = new Date(space.disabledAt); disabled.setDate(disabled.getDate() + _spaces.SPACE_MAX_DISABLED_DAYS); if (new Date() > disabled) { try { await this.sharesManager.deleteAllLinkMembers(space.id, _links.LINK_TYPE.SPACE); await this.deleteOrDisableSpace(space, true); this.logger.log(`${this.deleteExpiredSpaces.name} - space *${space.alias}* (${space.id}) was deleted`); } catch (e) { this.logger.error(`${this.deleteExpiredSpaces.name} - space *${space.alias}* (${space.id}) was not deleted : ${e}`); } } } } async listSpaceShares(user, spaceId) { if (await this.userIsSpaceManager(user, spaceId)) { return this.sharesManager.listSpaceShares(spaceId); } } async getSpaceShare(user, spaceId, shareId) { if (await this.userIsSpaceManager(user, spaceId, shareId)) { return this.sharesManager.getShareWithMembers(user, shareId, true); } } async updateSpaceShare(user, spaceId, shareId, createOrUpdateShareDto) { if (await this.userIsSpaceManager(user, spaceId, shareId)) { return this.sharesManager.updateShare(user, shareId, createOrUpdateShareDto, true); } } async deleteSpaceShare(user, spaceId, shareId) { if (await this.userIsSpaceManager(user, spaceId, shareId)) { return this.sharesManager.deleteShare(user, shareId, true); } } async getSpaceShareLink(user, spaceId, shareId) { if (await this.userIsSpaceManager(user, spaceId, shareId)) { return this.sharesManager.getShareLink(user, shareId, true); } } async updateMembers(user, space, currentMembers) { if (space.members.length === 0 && currentMembers.length === 0) { return; } // diff const [add, update, remove] = (0, _functions.diffCollection)(space.members, currentMembers, [ 'permissions', 'spaceRole' ], [ 'id', 'type' ]); // check members whitelists let toAdd = []; if (add.length) { const [userIdsWhitelist, groupIdsWhitelist] = await Promise.all([ this.usersQueries.usersWhitelist(user.id), this.usersQueries.groupsWhitelist(user.id) ]); toAdd = add.filter((m)=>{ if ((m.type === _member.MEMBER_TYPE.USER || m.type === _member.MEMBER_TYPE.GUEST) && !m.linkId && userIdsWhitelist.indexOf(m.id) === -1 || (m.type === _member.MEMBER_TYPE.GROUP || m.type === _member.MEMBER_TYPE.PGROUP) && groupIdsWhitelist.indexOf(m.id) === -1) { this.logger.warn(`${this.updateMembers.name} - cannot add ${m.type} (${m.id}) to space *${space.alias}* (${space.id}) : not in the members whitelist`); return false; } return true; }); } // filter links const toRemove = remove.filter((m)=>!m.linkId); // do remove links this.sharesManager.deleteLinkMembers(remove.filter((m)=>!!m.linkId)).catch((e)=>this.logger.error(`${this.updateMembers.name} - ${e}`)); // do update members const status = await this.spacesQueries.updateMembers(space.id, toAdd, (0, _functions.convertDiffUpdate)(update), toRemove); // lists deleted and updated members as potential share or space roots owners const [rmShareOwners, upShareOwners, rmRootOwners] = [ [], [], [] ]; for (const [action, members] of Object.entries(status)){ if (!members.userIds.length && !members.groupIds.length) continue; if (action === _constants.ACTION.DELETE) { // stores the removed members who might own shares created from the current space rmShareOwners.push(...toRemove.filter((m)=>(m.type === _member.MEMBER_TYPE.USER || m.type === _member.MEMBER_TYPE.GUEST) && members.userIds.indexOf(m.id) > -1 || (m.type === _member.MEMBER_TYPE.GROUP || m.type === _member.MEMBER_TYPE.PGROUP) && members.groupIds.indexOf(m.id) > -1)); // stores ids of removed members that might have space roots rmRootOwners.push(...members.userIds); } else if (action === _constants.ACTION.UPDATE) { // stores permissions updates and members who might own shares created from the current space // ignore space managers (they have all permissions) for (const m of update){ if ((m.object.type === _member.MEMBER_TYPE.USER || m.object.type === _member.MEMBER_TYPE.GUEST) && m.object.spaceRole !== _spaces.SPACE_ROLE.IS_MANAGER && members.userIds.indexOf(m.object.id) > -1 || (m.object.type === _member.MEMBER_TYPE.GROUP || m.object.type === _member.MEMBER_TYPE.PGROUP) && members.groupIds.indexOf(m.object.id) > -1) { // special case, the manager was moved to members without permissions change if (!m.permissions && m.spaceRole.new === _spaces.SPACE_ROLE.IS_MEMBER) { m.permissions = { old: _spaces.SPACE_ALL_OPERATIONS, new: m.object.permissions }; } // `share inside` permission is only used for spaces const diffPermissions = (0, _functions.differencePermissions)(m.permissions.old, m.permissions.new).filter((p)=>p !== _spaces.SPACE_OPERATION.SHARE_INSIDE); if (diffPermissions.length) { upShareOwners.push({ object: m.object, rmPermissions: diffPermissions }); } } } } // clear cache &|| notify this.onSpaceActionForMembers(space, action, members, user).catch((e)=>this.logger.error(`${this.updateMembers.name} - ${e}`)); } // do updates // remove or update potential shares this.sharesManager.updateSharesFromSpace(space.id, currentMembers, rmShareOwners, upShareOwners).catch((e)=>this.logger.error(`${this.updateMembers.name} - ${e}`)); // return potential root owner ids return rmRootOwners; } async updateRoots(user, space, curRoots, newRoots, aliases, names) { // diff const [add, toUpdate, toRemove] = (0, _functions.diffCollection)(curRoots, newRoots, [ 'name', 'alias', 'permissions' ]); // update for (const props of toUpdate){ if ('alias' in props) { props.alias.new = await this.uniqueRootAlias(space.id, props.alias.new, aliases.concat(names), true) || props.alias.new; // remove from space cache permissions this.spacesQueries.clearCachePermissions(space.alias, [ props.alias.old, props.alias.new ]).catch((e)=>this.logger.error(`${this.updateRoots.name} - ${e}`)); // update aliases list for next roots aliases.push(props.alias.new); } if ('name' in props) { props.name.new = this.uniqueRootName(props.name.new, names) || props.name.new; // update names list for next roots names.push(props.name.new); } } // format actions const toAdd = await this.validateNewRoots(user, space, curRoots, add, aliases, names); // remove a root implies that all shares with a reference to root.id will be deleted in cascade // however, it is "cleaner" to warn users about the deletion of these shares and to clear the permission caches before the cascade deletion // it must be assumed that the deletion and modifications of space roots will be successful const [rmRoots, rmRootPermissions] = [ toRemove.map((r)=>r.id), toUpdate.reduce((acc, r)=>{ if (r.permissions !== undefined) { const diffPermissions = (0, _functions.differencePermissions)(r.permissions.old, r.permissions.new); if (diffPermissions.length) { acc.push({ id: r.object.id, rmPermissions: diffPermissions }); } } return acc; }, []) ]; // do share updates await this.sharesManager.updateSharesFromSpaceRoots(space.id, rmRoots, rmRootPermissions); // do root updates const status = await this.spacesQueries.updateSpaceRoots(user.id, space.id, toAdd, (0, _functions.convertDiffUpdate)(toUpdate), toRemove); Object.entries(status).forEach(([action, roots])=>this.clearCachePermissionsAndOrNotify(space, action, user, null, roots).catch((e)=>this.logger.error(`${this.updateRoots.name} - ${e}`))); } async validateNewRoots(user, space, curRoots, newRoots, aliases, names) { const toAdd = []; for (const r of newRoots){ if (r.externalPath && !user.isAdmin) { this.logger.warn(`ignore new root *${r.alias}* (${r.externalPath}) : adding an external path requires the admin role`); continue; } const rPath = r.externalPath || _nodepath.default.join(user.filesPath, r.file.path); if (!await (0, _files.isPathExists)(rPath)) { this.logger.warn(`ignore new root *${r.alias}* (${r.file.path}) : *${rPath}* does not exist`); continue; } // check if a parent exists for an externalPath or if the file (or parent) is already anchored let rExists; if (r.externalPath) { rExists = curRoots.find((cr)=>cr.externalPath && r.externalPath.startsWith(cr.externalPath)); } else { rExists = curRoots.find((cr)=>cr.file?.id === r.file.id || cr.owner?.id === r.owner.id && cr.file?.path && r.file.path.startsWith(cr.file.path)); } if (rExists) { this.logger.warn(`ignore new root *${r.alias}* (${r.externalPath || r.file.path}) (${r.file?.id}) : parent or file already exists in roots`); continue; } // keep the file id (maybe already in db) if (!r.externalPath) { r.file = { ...await (0, _files.getProps)(rPath, r.file.path), id: r.file.id }; } r.alias = await this.uniqueRootAlias(space.id, r.alias, aliases.concat(names), true) || r.alias; // update aliases list for next roots aliases.push(r.alias); r.name = this.uniqueRootName(r.name, names) || r.name; // update names list for next roots names.push(r.name); // reset id r.id = 0; toAdd.push(r); } return toAdd; } async deleteOrDisableSpace(space, deleteNow = false) { if (deleteNow) { // clear cache &|| notify const memberIds = await this.spacesQueries.getSpaceMemberIds(space.id); this.onSpaceActionForMembers(space, _constants.ACTION.DELETE_PERMANENTLY, memberIds).catch((e)=>this.logger.error(`${this.deleteOrDisableSpace.name} - ${e}`)); // remove all shares related to the space await this.sharesManager.removeSharesFromSpace(space.id); } await this.spacesQueries.deleteSpace(space.id, deleteNow); if (deleteNow) { this.spacesQueries.cache.del(`${_cache.CACHE_QUOTA_SPACE_PREFIX}-${space.id}`).catch((e)=>this.logger.error(`${this.deleteOrDisableSpace.name} - ${e}`)); await this.deleteSpaceLocation(space.alias); } } async deleteSpaceLocation(spaceAlias) { const spaceLocation = _spacemodel.SpaceModel.getHomePath(spaceAlias); if (await (0, _files.isPathExists)(spaceLocation)) { await (0, _files.removeFiles)(spaceLocation); this.logger.warn(`${this.deleteSpaceLocation.name} - space *${spaceAlias}* location was deleted`); } else { this.logger.warn(`${this.deleteSpaceLocation.name} - space *${spaceAlias}* location does not exist : ${spaceLocation}`); } } async renameSpaceLocation(oldSpaceAlias, newSpaceAlias) { const currentSpaceLocation = _spacemodel.SpaceModel.getHomePath(oldSpaceAlias); if (await (0, _files.isPathExists)(currentSpaceLocation)) { const newSpaceLocation = _spacemodel.SpaceModel.getHomePath(newSpaceAlias); if (await (0, _files.isPathExists)(newSpaceLocation)) { this.logger.warn(`${this.renameSpaceLocation.name} - *${newSpaceAlias}* home path already exists : ${newSpaceLocation}`); return false; } else { try { await (0, _files.moveFiles)(currentSpaceLocation, newSpaceLocation); return true; } catch (e) { // try to restore await (0, _files.moveFiles)(newSpaceLocation, currentSpaceLocation, true); this.logger.error(`${this.renameSpaceLocation.name} - unable to rename space location from *${currentSpaceLocation}* to *${newSpaceLocation}* : ${e}`); return false; } } } else { this.logger.warn(`${this.renameSpaceLocation.name} - *${oldSpaceAlias}* space location does not exist : ${currentSpaceLocation}`); return false; } } async uniqueSpaceAlias(name, replaceCount = false) { let alias = (0, _shared.createSlug)(name, replaceCount); let count = 0; // Personal space name is reserved if (alias === _spaces.SPACE_ALIAS.PERSONAL) { count += 1; alias = `${name}-${count}`; } while(await this.spacesQueries.spaceExistsForAlias(alias)){ count += 1; alias = `${name}-${count}`; } return alias; } async userCanAccessSpace(userId, spaceId, asManager = false) { if (asManager) { // Get all space details if user is a manager const space = await this.spacesQueries.getSpaceAsManager(userId, spaceId); if (!space) { this.logger.warn(`space (${spaceId}) not found or not authorized for user (${userId})`); throw new _common.HttpException('Not authorized', _common.HttpStatus.FORBIDDEN); } return space; } else { const [space] = await this.spacesQueries.spaces(userId, true, spaceId); if (!space) { this.logger.warn(`space (${spaceId}) not found or not authorized for user (${userId})`); throw new _common.HttpException('Space not found', _common.HttpStatus.NOT_FOUND); } return space; } } async userIsSpaceManager(user, spaceId, shareId) { if (!await this.spacesQueries.userIsSpaceManager(user.id, spaceId, shareId)) { this.logger.warn(`space (${spaceId}) not found or not authorized for user (${user.id})`); throw new _common.HttpException('Not authorized', _common.HttpStatus.FORBIDDEN); } return true; } async setQuotaExceeded(user, space) { /* extract quota from spaces|shares|roots */ if (space.inSharesList) { return; } const cacheQuotaKey = (0, _paths.quotaKeyFromSpace)(user.id, space); if (!cacheQuotaKey) { this.logger.verbose(`${this.setQuotaExceeded.name} - quota was ignored for space : *${space.alias}* (${space.id})`); return; } let quota = await this.spacesQueries.cache.get(cacheQuotaKey); if (!quota) { // the quota scheduler has not started yet or the cache has been cleared if (space.inPersonalSpace) { quota = await this.updatePersonalSpacesQuota(user); } else if (space.inSharesRepository) { // Shares with external paths quota = await this.sharesManager.updateSharesExternalPathQuota(space.id); } else { quota = await this.updateSpacesQuota(space.id); } } if (quota) { space.storageUsage = quota.storageUsage; space.storageQuota = quota.storageQuota; space.quotaIsExceeded = quota.storageQuota !== null && quota.storageUsage >= quota.storageQuota; } else { this.logger.verbose(`${this.setQuotaExceeded.name} - quota not found for space : *${space.alias}* (${space.id})`); } } async onSpaceActionForMembers(space, action, members, user) { this.clearCachePermissionsAndOrNotify(space, action, user, Array.from(new Set([ ...await this.usersQueries.allUserIdsFromGroupsAndSubGroups(members.groupIds), ...members.userIds ])).filter((uid)=>uid !== user?.id)).catch((e)=>this.logger.error(`${this.onSpaceActionForMembers.name} - ${e}`)); } async clearCachePermissionsAndOrNotify(space, action, user, memberIds, roots) { if (memberIds?.length) { // clear permissions for space members if (action === _constants.ACTION.DELETE_PERMANENTLY) { this.logger.verbose(`${this.clearCachePermissionsAndOrNotify.name} - space:${space.alias} ${action}`); this.spacesQueries.clearCachePermissions(space.alias).catch((e)=>this.logger.error(`${this.clearCachePermissionsAndOrNotify.name} - ${e}`)); } else { // update space members cache this.logger.verbose(`${this.clearCachePermissionsAndOrNotify.name} - space:${space.alias} ${action} members:${JSON.stringify(memberIds)}`); this.spacesQueries.clearCachePermissions(space.alias, undefined, memberIds).catch((e)=>this.logger.error(`${this.clearCachePermissionsAndOrNotify.name} - ${e}`)); } // notify if (action !== _constants.ACTION.UPDATE) { // notify the members who have joined or left the space const notification = { app: _notifications.NOTIFICATION_APP.SPACES, event: _notifications.NOTIFICATION_APP_EVENT.SPACES[action], element: space.name, url: '' }; this.notificationsManager.create(memberIds, notification, { currentUrl: this.contextManager.headerOriginUrl(), action: action }).catch((e)=>this.logger.error(`${this.clearCachePermissionsAndOrNotify.name} - ${e}`)); } } else if (roots?.length) { // clear permissions for space roots const rootAliases = roots.map((r)=>r.alias); this.logger.verbose(`${this.clearCachePermissionsAndOrNotify.name} - space:${space.alias} ${action} roots:${JSON.stringify(rootAliases)}`); if (action !== _constants.ACTION.ADD) { this.spacesQueries.clearCachePermissions(space.alias, rootAliases).catch((e)=>this.logger.error(`${this.clearCachePermissionsAndOrNotify.name} - ${e}`)); } // notify if (action !== _constants.ACTION.UPDATE) { // notify the space members that a new root was anchored / unanchored const spaceMembers = await this.spacesQueries.getSpaceMemberIds(space.id); const spaceUserIds = Array.from(new Set([ ...await this.usersQueries.allUserIdsFromGroupsAndSubGroups(spaceMembers.groupIds), ...spaceMembers.userIds ])).filter((uid)=>uid !== user?.id); if (!spaceUserIds.length) { return; } for (const r of roots){ const notification = { app: _notifications.NOTIFICATION_APP.SPACE_ROOTS, event: _notifications.NOTIFICATION_APP_EVENT.SPACE_ROOTS[action], element: r.name, url: `${_spaces.SPACE_REPOSITORY.FILES}/${space.name}` }; this.notificationsManager.create(spaceUserIds, notification, { currentUrl: this.contextManager.headerOriginUrl(), author: user, action: action }).catch((e)=>this.logger.error(`${this.clearCachePermissionsAndOrNotify.name} - ${e}`)); } } } } constructor(contextManager, spacesQueries, usersQueries, sharesManager, notificationsManager){ this.contextManager = contextManager; this.spacesQueries = spacesQueries; this.usersQueries = usersQueries; this.sharesManager = sharesManager; this.notificationsManager = notificationsManager; this.logger = new _common.Logger(SpacesManager.name); } }; SpacesManager = _ts_decorate([ (0, _common.Injectable)(), _ts_metadata("design:type", Function), _ts_metadata("design:paramtypes", [ typeof _contextmanagerservice.ContextManager === "undefined" ? Object : _contextmanagerservice.ContextManager, typeof _spacesqueriesservice.SpacesQueries === "undefined" ? Object : _spacesqueriesservice.SpacesQueries, typeof _usersqueriesservice.UsersQueries === "undefined" ? Object : _usersqueriesservice.UsersQueries, typeof _sharesmanagerservice.SharesManager === "undefined" ? Object : _sharesmanagerservice.SharesManager, typeof _notificationsmanagerservice.NotificationsManager === "undefined" ? Object : _notificationsmanagerservice.NotificationsManager ]) ], SpacesManager); //# sourceMappingURL=spaces-manager.service.js.map