mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-15 10:37:43 +00:00
202 lines
6.3 KiB
JavaScript
202 lines
6.3 KiB
JavaScript
/*
|
|
* Copyright (C) 2024 Puter Technologies Inc.
|
|
*
|
|
* This file is part of Puter.
|
|
*
|
|
* Puter is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
const APIError = require("../../api/APIError");
|
|
const eggspress = require("../../api/eggspress");
|
|
const { FileFacade } = require("../../services/drivers/FileFacade");
|
|
const { TypeSpec } = require("../../services/drivers/meta/Construct");
|
|
const { TypedValue } = require("../../services/drivers/meta/Runtime");
|
|
const { Context } = require("../../util/context");
|
|
const { whatis } = require("../../util/langutil");
|
|
const { TeePromise } = require("../../util/promise");
|
|
const { valid_file_size } = require("../../util/validutil");
|
|
|
|
let _handle_multipart;
|
|
|
|
/**
|
|
* POST /drivers/call
|
|
*
|
|
* This endpoint is used to call methods offered by driver interfaces.
|
|
* The implementation used by each interface depends on the user's
|
|
* configuration.
|
|
*
|
|
* The request body can be a JSON object or multipart/form-data.
|
|
* For multipart/form-data, the caller must be aware that all fields
|
|
* are required to be sent before files so that the request handler
|
|
* and underlying driver implementation can decide what to do with
|
|
* file streams as they come.
|
|
*
|
|
* Example request body:
|
|
* {
|
|
* "interface": "puter-ocr",
|
|
* "method": "recognize",
|
|
* "args": {
|
|
* "file": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB...
|
|
* }
|
|
* }
|
|
*/
|
|
module.exports = eggspress('/drivers/call', {
|
|
subdomain: 'api',
|
|
auth2: true,
|
|
allowedMethods: ['POST'],
|
|
}, async (req, res, next) => {
|
|
const x = Context.get();
|
|
const svc_driver = x.get('services').get('driver');
|
|
|
|
let p_request = null;
|
|
let body;
|
|
if ( req.headers['content-type'].includes('multipart/form-data') ) {
|
|
({ params: body, p_data_end: p_request } = await _handle_multipart(req));
|
|
} else body = req.body;
|
|
|
|
const interface_name = body.interface;
|
|
const test_mode = body.test_mode;
|
|
|
|
let context = Context.get();
|
|
if ( test_mode ) context = context.sub({ test_mode: true });
|
|
|
|
const result = await context.arun(async () => {
|
|
return await svc_driver.call({
|
|
iface: interface_name,
|
|
driver: body.driver,
|
|
method: body.method,
|
|
format: body.format,
|
|
args: body.args,
|
|
});
|
|
});
|
|
|
|
// We can't wait for the request to finish before responding;
|
|
// consider the case where a driver method implements a
|
|
// stream transformation, thus the stream from the request isn't
|
|
// consumed until the response is being sent.
|
|
|
|
_respond(res, result);
|
|
|
|
// What we _can_ do is await the request promise while responding
|
|
// to ensure errors are caught here.
|
|
await p_request;
|
|
});
|
|
|
|
const _respond = (res, result) => {
|
|
if ( result.result instanceof TypedValue ) {
|
|
const tv = result.result;
|
|
debugger;
|
|
if ( TypeSpec.adapt({ $: 'stream' }).equals(tv.type) ) {
|
|
res.set('Content-Type', tv.type.raw.content_type);
|
|
if ( tv.type.raw.chunked ) {
|
|
res.set('Transfer-Encoding', 'chunked');
|
|
}
|
|
tv.value.pipe(res);
|
|
return;
|
|
}
|
|
|
|
// This is the
|
|
if ( typeof result.value === 'object' ) {
|
|
result.value.type_fallback = true;
|
|
}
|
|
res.json(result.value);
|
|
return;
|
|
}
|
|
res.json(result);
|
|
};
|
|
|
|
_handle_multipart = async (req) => {
|
|
const Busboy = require('busboy');
|
|
const { PassThrough } = require('stream');
|
|
|
|
const params = {};
|
|
const files = [];
|
|
let file_index = 0;
|
|
|
|
const bb = Busboy({
|
|
headers: req.headers,
|
|
});
|
|
|
|
const p_data_end = new TeePromise();
|
|
const p_nonfile_data_end = new TeePromise();
|
|
bb.on('file', (fieldname, stream, details) => {
|
|
p_nonfile_data_end.resolve();
|
|
const fileinfo = files[file_index++];
|
|
stream.pipe(fileinfo.stream);
|
|
});
|
|
|
|
const on_field = (fieldname, value) => {
|
|
const key_parts = fieldname.split('.');
|
|
const last_key = key_parts.pop();
|
|
let dst = params;
|
|
for ( let i = 0; i < key_parts.length; i++ ) {
|
|
if ( ! dst.hasOwnProperty(key_parts[i]) ) {
|
|
dst[key_parts[i]] = {};
|
|
}
|
|
if ( whatis(dst[key_parts[i]]) !== 'object' ) {
|
|
throw new Error(
|
|
`Tried to set member of non-object: ${key_parts[i]} in ${fieldname}`
|
|
);
|
|
}
|
|
dst = dst[key_parts[i]];
|
|
}
|
|
if ( whatis(value) === 'object' && value.$ === 'file' ) {
|
|
const fileinfo = value;
|
|
const { v: size, ok: size_ok } =
|
|
valid_file_size(fileinfo.size);
|
|
if ( ! size_ok ) {
|
|
throw APIError.create('invalid_file_metadata');
|
|
}
|
|
fileinfo.size = size;
|
|
fileinfo.stream = new PassThrough();
|
|
const file_facade = new FileFacade();
|
|
file_facade.values.set('stream', fileinfo.stream);
|
|
fileinfo.facade = file_facade,
|
|
files.push(fileinfo);
|
|
value = file_facade;
|
|
}
|
|
if ( dst.hasOwnProperty(last_key) ) {
|
|
if ( ! Array.isArray(dst[last_key]) ) {
|
|
dst[last_key] = [dst[last_key]];
|
|
}
|
|
dst[last_key].push(value);
|
|
} else {
|
|
dst[last_key] = value;
|
|
}
|
|
};
|
|
|
|
bb.on('field', (fieldname, value, details) => {
|
|
const o = JSON.parse(value);
|
|
for ( const k in o ) {
|
|
on_field(k, o[k]);
|
|
}
|
|
});
|
|
bb.on('error', (err) => {
|
|
p_data_end.reject(err);
|
|
});
|
|
bb.on('close', () => {
|
|
p_data_end.resolve();
|
|
});
|
|
|
|
req.pipe(bb);
|
|
|
|
(async () => {
|
|
await p_data_end;
|
|
p_nonfile_data_end.resolve();
|
|
})();
|
|
|
|
await p_nonfile_data_end;
|
|
|
|
return { params, p_data_end };
|
|
}
|