@sync-in/server
Version:
The secure, open-source platform for file storage, sharing, collaboration, and sync
824 lines (823 loc) • 44 kB
JavaScript
/*
* 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