feat: update share endpoint to support more things

This commit is contained in:
KernelDeimos 2024-06-20 04:52:30 -04:00 committed by Eric Dubé
parent 18f9959776
commit dd5fde5130
5 changed files with 206 additions and 59 deletions

View File

@ -30,9 +30,10 @@ anywhere.
## Specification
- The `"$"` key indicates a type (or class) of object
- Any key beginning with `$` is a **meta-key**
- Any other key beginning with `$` is a **meta-key**
- Other keys are not allowed to contain `$`
- `"$version"` must follow [semver](https://semver.org/)
- Keys with multiple `"$"` symbols are reserved for future use
## Alternative Representations

View File

@ -57,6 +57,9 @@ const install = async ({ services, app, useapi }) => {
const ArrayUtil = require('./libraries/ArrayUtil');
services.registerService('util-array', ArrayUtil);
const LibTypeTagged = require('./libraries/LibTypeTagged');
services.registerService('lib-type-tagged', LibTypeTagged);
// === SERVICES ===
// /!\ IMPORTANT /!\

View File

@ -33,6 +33,29 @@ module.exports = class APIError {
status: 500,
message: () => `An unknown error occurred`,
},
'format_error': {
status: 400,
message: ({ message }) => `format error: ${message}`,
},
'temp_error': {
status: 400,
message: ({ message }) => `error: ${message}`,
},
// Things
'disallowed_thing': {
status: 400,
message: ({ thing_type, accepted }) =>
`Request contained a ${quot(thing_type)} in a ` +
`place where ${quot(thing_type)} isn't accepted` +
(
accepted
? '; ' +
'accepted types are: ' +
accepted.map(v => quot(v)).join(', ')
: ''
) + '.'
},
// Unorganized
'item_with_same_name_exists': {

View File

@ -0,0 +1,88 @@
const Library = require("../definitions/Library");
const { whatis } = require("../util/langutil");
class LibTypeTagged extends Library {
process (o) {
const could_be = whatis(o) === 'object' || Array.isArray(o);
if ( ! could_be ) return {
$: 'error',
code: 'invalid-type',
message: 'should be object or array',
};
const intermediate = this.get_intermediate_(o);
if ( ! intermediate.type ) return {
$: 'error',
code: 'missing-type-param',
message: 'type parameter is missing',
};
return this.intermediate_to_standard_(intermediate);
}
intermediate_to_standard_ (intermediate) {
const out = {};
out.$ = intermediate.type;
for ( const k in intermediate.meta ) {
out['$' + k] = intermediate.meta[k];
}
for ( const k in intermediate.body ) {
out[k] = intermediate.body[k];
}
return out;
}
get_intermediate_ (o) {
if ( Array.isArray(o) ) {
return this.process_array_(o);
}
if ( o['$'] === '$meta-body' ) {
return this.process_structured_(o);
}
return this.process_standard_(o);
}
process_array_ (a) {
if ( a.length <= 1 || a.length > 3 ) return {
$: 'error',
code: 'invalid-array-length',
message: 'tag-typed arrays should have 1-3 elements',
};
const [type, body = {}, meta = {}] = a;
return { $: '$', type, body, meta };
}
process_structured_ (o) {
if ( ! o.hasOwnProperty('type') ) return {
$: 'error',
code: 'missing-type-property',
message: 'missing "type" property'
};
return { $: '$', ...o };
}
process_standard_ (o) {
const type = o.$;
const meta = {};
const body = {};
for ( const k in o ) {
if ( k === '$' ) continue;
if ( k.startsWith('$') ) {
meta[k.slice(1)] = o[k];
} else {
body[k] = o[k];
}
}
return { $: '$', type, meta, body };
}
}
module.exports = LibTypeTagged;

View File

@ -3,7 +3,7 @@ const { Endpoint } = require('../util/expressutil');
const validator = require('validator');
const APIError = require('../api/APIError');
const { get_user } = require('../helpers');
const { get_user, get_app } = require('../helpers');
const { Context } = require('../util/context');
const auth2 = require('../middleware/auth2');
const config = require('../config');
@ -140,6 +140,8 @@ const v0_2 = async (req, res) => {
const svc_permission = req.services.get('permission');
const svc_notification = req.services.get('notification');
const lib_typeTagged = req.services.get('lib-type-tagged');
const actor = Context.get('actor');
// === Request Validators ===
@ -170,12 +172,12 @@ const v0_2 = async (req, res) => {
return recipients;
});
const validate_paths = UtilFn(paths => {
const validate_shares = UtilFn(shares => {
// Single-values get adapted into an array
if ( ! Array.isArray(paths) ) {
paths = [paths];
if ( ! Array.isArray(shares) ) {
shares = [shares];
}
return paths;
return shares;
})
// === Request Values ===
@ -184,8 +186,8 @@ const v0_2 = async (req, res) => {
validate_mode.if(req.body.mode) ?? false;
const req_recipients =
validate_recipients.if(req.body.recipients) ?? [];
const req_paths =
validate_paths.if(req.body.paths) ?? [];
const req_shares =
validate_shares.if(req.body.shares) ?? [];
// === State Values ===
@ -198,10 +200,10 @@ const v0_2 = async (req, res) => {
// Results
status: null,
recipients: Array(req_recipients.length).fill(null),
paths: Array(req_paths.length).fill(null),
shares: Array(req_shares.length).fill(null),
}
const recipients_work = new WorkList();
const fsitems_work = new WorkList();
const shares_work = new WorkList();
// const assert_work_item = (wut, item) => {
// if ( item.$ !== wut ) {
@ -221,11 +223,11 @@ const v0_2 = async (req, res) => {
result.recipients[i] = result.recipients[i].serialize();
}
}
for ( let i=0 ; i < result.paths.length ; i++ ) {
if ( ! result.paths[i] ) continue;
if ( result.paths[i] instanceof APIError ) {
for ( let i=0 ; i < result.shares.length ; i++ ) {
if ( ! result.shares[i] ) continue;
if ( result.shares[i] instanceof APIError ) {
result.status = 'mixed';
result.paths[i] = result.paths[i].serialize();
result.shares[i] = result.shares[i].serialize();
}
}
};
@ -234,7 +236,7 @@ const v0_2 = async (req, res) => {
console.log('OK');
if (
result.recipients.some(v => v !== null) ||
result.paths.some(v => v !== null)
result.shares.some(v => v !== null)
) {
console.log('DOESNT THIS??')
serialize_result();
@ -329,74 +331,104 @@ const v0_2 = async (req, res) => {
// --- Process Paths ---
// Expect: at least one path
if ( req_paths.length < 1 ) {
if ( req_shares.length < 1 ) {
throw APIError.create('field_invalid', null, {
key: 'paths',
key: 'shares',
expected: 'at least one',
got: 'none',
})
}
for ( let i=0 ; i < req_paths.length ; i++ ) {
const value = req_paths[i];
fsitems_work.push({ i, value });
for ( let i=0 ; i < req_shares.length ; i++ ) {
const value = req_shares[i];
shares_work.push({ i, value });
}
fsitems_work.lockin();
shares_work.lockin();
for ( const item of fsitems_work.list() ) {
for ( const item of shares_work.list() ) {
const { i } = item;
let { value } = item;
// adapt all strings to objects
if ( typeof value === 'string' ) {
value = { path: value };
}
if ( whatis(value) !== 'object' ) {
const thing = lib_typeTagged.process(value);
if ( thing.$ === 'error' ) {
item.invalid = true;
result.paths[i] =
APIError.create('invalid_path', null, {
path: item.path,
value,
result.shares[i] =
APIError.create('format_error', null, {
message: thing.message
});
continue;
}
const errors = [];
if ( ! value.path ) {
errors.push('`path` is required');
console.log('thing?', thing);
const allowed_things = ['fs-share', 'app-share'];
if ( ! allowed_things.includes(thing.$) ) {
APIError.create('disallowed_thing', null, {
thing: thing.$,
accepted: allowed_things,
})
}
let access = value.access;
if ( access ) {
if ( ! ['read','write'].includes(access) ) {
errors.push('`access` should be `read` or `write`');
if ( thing.$ === 'fs-share' ) {
item.type = 'fs';
const errors = [];
if ( ! thing.path ) {
errors.push('`path` is required');
}
} else access = 'read';
let access = thing.access;
if ( access ) {
if ( ! ['read','write'].includes(access) ) {
errors.push('`access` should be `read` or `write`');
}
} else access = 'read';
if ( errors.length ) {
item.invalid = true;
result.paths[item.i] =
APIError.create('field_errors', null, {
path: item.path,
errors
});
continue;
if ( errors.length ) {
item.invalid = true;
result.shares[item.i] =
APIError.create('field_errors', null, {
key: `shares[${item.i}]`,
errors
});
continue;
}
item.path = thing.path;
item.permission = PermissionUtil.join('fs', thing.path, access);
}
item.path = value.path;
item.permission = PermissionUtil.join('fs', value.path, access);
if ( thing.$ === 'app-share' ) {
item.type = 'app';
const errors = [];
if ( ! thing.uid && thing.name ) {
errors.push('`uid` or `name` is required');
}
if ( errors.length ) {
item.invalid = true;
result.shares[item.i] =
APIError.create('field_errors', null, {
key: `shares[${item.i}]`,
errors
});
continue;
}
item.permission = PermissionUtil.join('app', thing.path, 'access');
continue;
}
}
fsitems_work.clear_invalid();
shares_work.clear_invalid();
for ( const item of fsitems_work.list() ) {
for ( const item of shares_work.list() ) {
if ( item.type !== 'fs' ) continue;
const node = await (new FSNodeParam('path')).consolidate({
req, getParam: () => item.path
});
if ( ! await node.exists() ) {
item.invalid = true;
result.paths[item.i] = APIError.create('subject_does_not_exist', {
result.shares[item.i] = APIError.create('subject_does_not_exist', {
path: item.path,
})
continue;
@ -417,12 +449,12 @@ const v0_2 = async (req, res) => {
item.email_link = email_link;
}
fsitems_work.clear_invalid();
shares_work.clear_invalid();
// Mark files as successful; further errors will be
// reported on recipients instead.
for ( const item of fsitems_work.list() ) {
result.paths[item.i] =
for ( const item of shares_work.list() ) {
result.shares[item.i] =
{
$: 'api:status-report',
status: 'success',
@ -452,11 +484,11 @@ const v0_2 = async (req, res) => {
const username = recipient_item.user.username;
for ( const path_item of fsitems_work.list() ) {
for ( const share_item of shares_work.list() ) {
await svc_permission.grant_user_user_permission(
actor,
username,
path_item.permission,
share_item.permission,
);
}
@ -478,7 +510,7 @@ const v0_2 = async (req, res) => {
*/
const files = []; {
for ( const path_item of fsitems_work.list() ) {
for ( const path_item of shares_work.list() ) {
files.push(
await path_item.node.getSafeEntry(),
);