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 ## Specification
- The `"$"` key indicates a type (or class) of object - 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 `$` - Other keys are not allowed to contain `$`
- `"$version"` must follow [semver](https://semver.org/) - `"$version"` must follow [semver](https://semver.org/)
- Keys with multiple `"$"` symbols are reserved for future use
## Alternative Representations ## Alternative Representations

View File

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

View File

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