diff --git a/src/backend/exports.js b/src/backend/exports.js
index 77bc94024..a8649af66 100644
--- a/src/backend/exports.js
+++ b/src/backend/exports.js
@@ -25,6 +25,7 @@ const PuterDriversModule = require("./src/PuterDriversModule.js");
const { testlaunch } = require("./src/index.js");
const BaseService = require("./src/services/BaseService.js");
const { Context } = require("./src/util/context.js");
+const { TestDriversModule } = require("./src/modules/test-drivers/TestDriversModule.js");
module.exports = {
@@ -46,4 +47,5 @@ module.exports = {
PuterDriversModule,
LocalDiskStorageModule,
SelfHostedModule,
+ TestDriversModule,
};
diff --git a/src/backend/src/data/hardcoded-permissions.js b/src/backend/src/data/hardcoded-permissions.js
index 26ba332fb..8308d123f 100644
--- a/src/backend/src/data/hardcoded-permissions.js
+++ b/src/backend/src/data/hardcoded-permissions.js
@@ -72,6 +72,9 @@ const policy_perm = selector => ({
const hardcoded_user_group_permissions = {
system: {
+ 'ca342a5e-b13d-4dee-9048-58b11a57cc55': {
+ 'service': {},
+ },
'b7220104-7905-4985-b996-649fdcdb3c8f': {
'service:hello-world:ii:hello-world': policy_perm('temp.es'),
'driver:puter-kvstore': policy_perm('temp.kv'),
diff --git a/src/backend/src/modules/test-drivers/TestAssetHostService.js b/src/backend/src/modules/test-drivers/TestAssetHostService.js
new file mode 100644
index 000000000..dd991959e
--- /dev/null
+++ b/src/backend/src/modules/test-drivers/TestAssetHostService.js
@@ -0,0 +1,16 @@
+const BaseService = require("../../services/BaseService");
+
+class TestAssetHostService extends BaseService {
+ async ['__on_install.routes'] () {
+ const { app } = this.services.get('web-server');
+ const path_ = require('node:path');
+
+ app.use('/test-assets', require('express').static(
+ path_.join(__dirname, 'assets')
+ ));
+ }
+}
+
+module.exports = {
+ TestAssetHostService
+};
diff --git a/src/backend/src/modules/test-drivers/TestDriversModule.js b/src/backend/src/modules/test-drivers/TestDriversModule.js
new file mode 100644
index 000000000..609324a18
--- /dev/null
+++ b/src/backend/src/modules/test-drivers/TestDriversModule.js
@@ -0,0 +1,17 @@
+const { AdvancedBase } = require("@heyputer/puter-js-common");
+
+class TestDriversModule extends AdvancedBase {
+ async install (context) {
+ const services = context.get('services');
+
+ const { TestAssetHostService } = require('./TestAssetHostService')
+ services.registerService('__test-assets', TestAssetHostService);
+
+ const { TestImageService } = require('./TestImageService');
+ services.registerService('test-image', TestImageService);
+ }
+}
+
+module.exports = {
+ TestDriversModule,
+};
diff --git a/src/backend/src/modules/test-drivers/TestImageService.js b/src/backend/src/modules/test-drivers/TestImageService.js
new file mode 100644
index 000000000..4c0603042
--- /dev/null
+++ b/src/backend/src/modules/test-drivers/TestImageService.js
@@ -0,0 +1,85 @@
+const config = require("../../config");
+const BaseService = require("../../services/BaseService");
+const { TypedValue } = require("../../services/drivers/meta/Runtime");
+const { buffer_to_stream } = require("../../util/streamutil");
+
+const PUBLIC_DOMAIN_IMAGES = [
+ {
+ name: 'starry-night',
+ url: 'https://upload.wikimedia.org/wikipedia/commons/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg',
+ file: 'starry.jpg',
+ },
+];
+
+class TestImageService extends BaseService {
+ async ['__on_driver.register.interfaces'] () {
+ const svc_registry = this.services.get('registry');
+ const col_interfaces = svc_registry.get('interfaces');
+
+ col_interfaces.set('test-image', {
+ methods: {
+ echo_image: {
+ parameters: {
+ source: {
+ type: 'file',
+ },
+ },
+ result: {
+ type: {
+ $: 'stream',
+ content_type: 'image'
+ },
+ },
+ },
+ get_image: {
+ parameters: {
+ source_type: {
+ type: 'string'
+ },
+ },
+ result: {
+ type: {
+ $: 'stream',
+ content_type: 'image'
+ }
+ }
+ }
+ }
+ });
+ }
+
+ static IMPLEMENTS = {
+ ['version']: {
+ get_version () {
+ return 'v1.0.0';
+ }
+ },
+ ['test-image']: {
+ async echo_image ({
+ source,
+ }) {
+ const stream = await source.get('stream');
+ return new TypedValue({
+ $: 'stream',
+ content_type: 'image/jpeg'
+ }, stream);
+ },
+ async get_image ({
+ source_type,
+ }) {
+ const image = PUBLIC_DOMAIN_IMAGES[0];
+ if ( source_type === 'string:url:web' ) {
+ return new TypedValue({
+ $: 'string:url:web',
+ content_type: 'image',
+ }, `${config.origin}/test-assets/${image.file}`);
+ }
+ throw new Error('not implemented yet');
+ }
+ },
+ }
+}
+
+module.exports = {
+ TestImageService
+};
diff --git a/src/backend/src/modules/test-drivers/assets/starry.jpg b/src/backend/src/modules/test-drivers/assets/starry.jpg
new file mode 100644
index 000000000..9a24b899d
Binary files /dev/null and b/src/backend/src/modules/test-drivers/assets/starry.jpg differ
diff --git a/src/backend/src/modules/test-drivers/assets/wave.jpg b/src/backend/src/modules/test-drivers/assets/wave.jpg
new file mode 100644
index 000000000..cf6a3940d
Binary files /dev/null and b/src/backend/src/modules/test-drivers/assets/wave.jpg differ
diff --git a/src/backend/src/modules/test-drivers/doc/requests.md b/src/backend/src/modules/test-drivers/doc/requests.md
new file mode 100644
index 000000000..39275fb9b
--- /dev/null
+++ b/src/backend/src/modules/test-drivers/doc/requests.md
@@ -0,0 +1,98 @@
+```javascript
+blob = await (await fetch("http://api.puter.localhost:4100/drivers/call", {
+ "headers": {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${puter.authToken}`,
+ },
+ "body": JSON.stringify({
+ interface: 'test-image',
+ method: 'get_image',
+ args: {
+ source_type: 'string:url:web'
+ }
+ }),
+ "method": "POST",
+})).blob();
+dataurl = await new Promise((y, n) => {
+ a = new FileReader();
+ a.onload = _ => y(a.result);
+ a.onerror = _ => n(a.error);
+ a.readAsDataURL(blob)
+});
+URL.createObjectURL(await (await fetch("http://api.puter.localhost:4100/drivers/call", {
+ "headers": {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${puter.authToken}`,
+ },
+ "body": JSON.stringify({
+ interface: 'test-image',
+ method: 'echo_image',
+ args: {
+ source: dataurl,
+ }
+ }),
+ "method": "POST",
+})).blob());
+```
+
+```javascript
+await(async () => {
+
+ blob = await (await fetch("http://api.puter.localhost:4100/drivers/call", {
+ "headers": {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${puter.authToken}`,
+ },
+ "body": JSON.stringify({
+ interface: 'test-image',
+ method: 'get_image',
+ args: {
+ source_type: 'string:url:web'
+ }
+ }),
+ "method": "POST",
+ })).blob();
+
+ const endpoint = 'http://api.puter.localhost:4100/drivers/call';
+
+ const body = {
+ object: {
+ interface: 'test-image',
+ method: 'echo_image',
+ ['args.source']: {
+ $: 'file',
+ size: blob.size,
+ type: blob.type,
+ },
+ },
+ file: [
+ blob,
+ ]
+ };
+
+ const formData = new FormData();
+ for ( const k in body ) {
+ console.log('k', k);
+ const append = v => {
+ if ( v instanceof Blob ) {
+ formData.append(k, v, 'filename');
+ } else {
+ formData.append(k, JSON.stringify(v));
+ }
+ };
+ if ( Array.isArray(body[k]) ) {
+ for ( const v of body[k] ) append(v);
+ } else {
+ append(body[k]);
+ }
+ }
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers: { 'Authorization': `Bearer ${puter.authToken}` },
+ body: formData
+ });
+ const echo_blob = await response.blob();
+ const echo_url = URL.createObjectURL(echo_blob);
+ return echo_url;
+})();
+```
\ No newline at end of file
diff --git a/src/backend/src/routers/drivers/call.js b/src/backend/src/routers/drivers/call.js
index 8d0ff7aa9..737231af1 100644
--- a/src/backend/src/routers/drivers/call.js
+++ b/src/backend/src/routers/drivers/call.js
@@ -16,12 +16,15 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
+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;
@@ -55,12 +58,14 @@ module.exports = eggspress('/drivers/call', {
const x = Context.get();
const svc_driver = x.get('services').get('driver');
- const interface_name = req.body.interface;
- const test_mode = req.body.test_mode;
+ 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 args = req.headers['content-type'].includes('multipart/form-data')
- ? await _handle_multipart(req)
- : req.body.args;
+ const interface_name = body.interface;
+ const test_mode = body.test_mode;
let context = Context.get();
if ( test_mode ) context = context.sub({ test_mode: true });
@@ -68,12 +73,22 @@ module.exports = eggspress('/drivers/call', {
const result = await context.arun(async () => {
return await svc_driver.call({
iface: interface_name,
- method: req.body.method,
- args
+ 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) => {
@@ -96,49 +111,86 @@ const _respond = (res, result) => {
};
_handle_multipart = async (req) => {
- const busboy = require('busboy');
- const { Readable } = require('stream');
+ const Busboy = require('busboy');
+ const { PassThrough } = require('stream');
const params = {};
+ const files = [];
+ let file_index = 0;
- const bb = new busboy({
+ const bb = Busboy({
headers: req.headers,
});
- const p_ready = new TeePromise();
+ const p_data_end = new TeePromise();
+ const p_nonfile_data_end = new TeePromise();
bb.on('file', (fieldname, stream, details) => {
- const file_facade = new FileFacade();
- file_facade.values.set('stream', stream);
- file_facade.values.set('busboy:details', details);
- if ( params.hasOwnProperty(fieldname) ) {
- if ( ! Array.isArray(params[fieldname]) ) {
- params[fieldname] = [params[fieldname]];
- }
- params[fieldname].push(file_facade);
- } else {
- params[fieldname] = file_facade;
- }
+ p_nonfile_data_end.resolve();
+ const fileinfo = files[file_index++];
+ stream.pipe(fileinfo.stream);
});
- bb.on('field', (fieldname, value, details) => {
- if ( params.hasOwnProperty(fieldname) ) {
- if ( ! Array.isArray(params[fieldname]) ) {
- params[fieldname] = [params[fieldname]];
+
+ 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]] = {};
}
- params[fieldname].push(value);
+ 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 {
- params[fieldname] = value;
+ 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_ready.reject(err);
+ p_data_end.reject(err);
});
bb.on('close', () => {
- p_ready.resolve();
+ p_data_end.resolve();
});
req.pipe(bb);
- await p_ready;
+ (async () => {
+ await p_data_end;
+ p_nonfile_data_end.resolve();
+ })();
- return params;
-}
\ No newline at end of file
+ await p_nonfile_data_end;
+
+ return { params, p_data_end };
+}
diff --git a/src/backend/src/services/drivers/CoercionService.js b/src/backend/src/services/drivers/CoercionService.js
index bd64018b5..c3784e178 100644
--- a/src/backend/src/services/drivers/CoercionService.js
+++ b/src/backend/src/services/drivers/CoercionService.js
@@ -40,6 +40,7 @@ class CoercionService extends BaseService {
content_type: 'image'
},
coerce: async typed_value => {
+ this.log.noticeme('coercion is running!');
const response = await CoercionService.MODULES.axios.get(typed_value.value, {
responseType: 'stream',
});
diff --git a/src/backend/src/services/drivers/DriverService.js b/src/backend/src/services/drivers/DriverService.js
index 510b017fc..c9f427759 100644
--- a/src/backend/src/services/drivers/DriverService.js
+++ b/src/backend/src/services/drivers/DriverService.js
@@ -111,6 +111,7 @@ class DriverService extends BaseService {
}
async _call ({ driver, iface, method, args }) {
+ console.log('??', driver, iface, method, args);
const processed_args = await this._process_args(iface, method, args);
if ( Context.get('test_mode') ) {
processed_args.test_mode = true;
@@ -293,7 +294,7 @@ class DriverService extends BaseService {
{
name: 'enforce logical rate-limit',
on_call: async args => {
- if ( ! effective_policy['rate-limit'] ) return args;
+ if ( ! effective_policy?.['rate-limit'] ) return args;
const svc_su = this.services.get('su');
const svc_rateLimit = this.services.get('rate-limit');
await svc_su.sudo(policy_holder, async () => {
@@ -309,7 +310,7 @@ class DriverService extends BaseService {
{
name: 'enforce monthly usage limit',
on_call: async args => {
- if ( ! effective_policy['monthly-limit'] ) return args;
+ if ( ! effective_policy?.['monthly-limit'] ) return args;
const svc_monthlyUsage = services.get('monthly-usage');
const count = await svc_monthlyUsage.check_2(
actor, method_key
@@ -356,9 +357,18 @@ class DriverService extends BaseService {
name: 'result coercion',
on_return: async (result) => {
if ( result instanceof TypedValue ) {
+ const svc_registry = this.services.get('registry');
+ const c_interfaces = svc_registry.get('interfaces');
+
+ console.log('????--1', iface);
const interface_ = c_interfaces.get(iface);
- let desired_type = interface_.methods[method]
- .result_choices[0].type;
+ console.log('????--2', interface_);
+ const method_spec = interface_.methods[method];
+ let desired_type =
+ method_spec.result_choices
+ ? method_spec.result_choices[0].type
+ : method_spec.result.type
+ ;
const svc_coercion = services.get('coercion');
result = await svc_coercion.coerce(desired_type, result);
}
diff --git a/src/backend/src/services/drivers/types.js b/src/backend/src/services/drivers/types.js
index cf3813852..1c227bd9f 100644
--- a/src/backend/src/services/drivers/types.js
+++ b/src/backend/src/services/drivers/types.js
@@ -98,6 +98,10 @@ class File extends BaseType {
}
async consolidate (ctx, input, { arg_name }) {
+ if ( input instanceof FileFacade ) {
+ return input;
+ }
+
const result = new FileFacade();
// DRY: Part of this is duplicating FSNodeParam, but FSNodeParam is
// subject to change in PR #647, so this should be updated later.
diff --git a/src/gui/src/i18n/translations/ko.js b/src/gui/src/i18n/translations/ko.js
index e2d78c5d9..47fca4779 100644
--- a/src/gui/src/i18n/translations/ko.js
+++ b/src/gui/src/i18n/translations/ko.js
@@ -349,4 +349,4 @@ const ko = {
},
};
-export default ko;
+export default ko;
\ No newline at end of file
diff --git a/src/gui/src/i18n/translations/nl.js b/src/gui/src/i18n/translations/nl.js
index d02da55d2..e4bc171b0 100644
--- a/src/gui/src/i18n/translations/nl.js
+++ b/src/gui/src/i18n/translations/nl.js
@@ -18,190 +18,334 @@
*/
const nl = {
- name: "Nederlands",
- english_name: "Dutch",
- code: "nl",
+ name: 'Nederlands',
+ english_name: 'Dutch',
+ code: 'nl',
dictionary: {
- about: "Over",
- account: "Account",
- access_granted_to: "Toegang gegeven aan",
- add_existing_account: "Bestaand Account Toevoegen",
+ about: 'Over',
+ account: 'Account',
+ account_password: 'Verifieer Account Wachtwoord',
+ access_granted_to: 'Toegang gegeven aan',
+ add_existing_account: 'Bestaand Account Toevoegen',
all_fields_required: 'Alle velden zijn vereist.',
- apply: "Toepassen",
+ allow: 'Toestaan',
+ apply: 'Toepassen',
ascending: 'Oplopend',
+ associated_websites: 'Geassocieerde Websites',
auto_arrange: 'Automatisch sorteren',
- background: "Achtergrond",
- browse: "Verkennen",
+ background: 'Achtergrond',
+ browse: 'Bladeren',
cancel: 'Annuleren',
center: 'Centreren',
- change_desktop_background: 'Achtergrond veranderen…',
- change_language: "Taal veranderen",
- change_password: "Wachtwoord veranderen",
- change_username: "Gebruikersnaam veranderen",
- close_all_windows: "Alle schermen sluiten",
- close_all_windows_and_log_out: 'Alle schermen sluiten en afmelden',
- change_always_open_with: "Dit type bestand altijd openen met",
+ change_desktop_background: 'Bureaubladachtergrond veranderen…',
+ change_email: 'E-mail Wijzigen',
+ change_language: 'Taal veranderen',
+ change_password: 'Wachtwoord veranderen',
+ change_ui_colors: 'UI Kleuren veranderen',
+ change_username: 'Gebruikersnaam veranderen',
+ close: 'Sluiten',
+ close_all_windows: 'Alle vensters sluiten',
+ close_all_windows_confirm: 'Weet u zeker dat u alle vensters wilt sluiten?',
+ close_all_windows_and_log_out: 'Vensters sluiten en afmelden',
+ change_always_open_with: 'Wilt u dit bestandstype altijd openen met',
color: 'Kleur',
confirm: 'Bevestig',
- confirm_account_for_free_referral_storage_c2a: 'Maak een account en bevestig uw emailadres om 1 GB aan gratis opslag te ontvangen. Uw vriend krijgt ook 1 GB aan gratis opslag.',
- confirm_delete_multiple_items: 'Weet u zeker dat u deze bestanden permanent wilt verwijderen?',
- confirm_delete_single_item: 'Weet u zeker dat u dit bestand permanent wilt verwijderen?',
- confirm_delete_user: 'Weet u zeker dat u uw account wilt verwijderen? Al uw bestanden en gegevens zal permanent verwijderd worden. Deze actie kan niet ongedaan gemaakt worden.',
- confirm_open_apps_log_out: 'U heeft nog geopende applicaties. Weet u zeker dat u wilt uitloggen?',
- confirm_new_password: "Bevestig Nieuw Wachtwoord",
- contact_us: "Neem contact met ons op",
+ confirm_2fa_setup: 'Ik heb de code toegevoegd aan mijn authenticatie-app',
+ confirm_2fa_recovery: 'Ik heb mijn herstelcodes op een veilige plaats opgeslagen',
+ confirm_account_for_free_referral_storage_c2a: 'Maak een account aan en bevestig uw e-mailadres om 1 GB gratis opslag te ontvangen. Uw vriend krijgt ook 1 GB gratis opslag.',
+ confirm_code_generic_incorrect: 'Onjuiste code.',
+ confirm_code_generic_too_many_requests: 'Te veel aanvragen. Wacht een paar minuten.',
+ confirm_code_generic_submit: 'Code invoeren',
+ confirm_code_generic_try_again: 'Probeer opnieuw',
+ confirm_code_generic_title: 'Bevestigingscode Invoeren',
+ confirm_code_2fa_instruction: 'Voer de 6-cijferige code in van uw authenticatie-app.',
+ confirm_code_2fa_submit_btn: 'Verzenden',
+ confirm_code_2fa_title: 'Voer 2FA Code In',
+ confirm_delete_multiple_items: 'Weet u zeker dat u deze items permanent wilt verwijderen?',
+ confirm_delete_single_item: 'Wilt u dit item permanent verwijderen?',
+ confirm_open_apps_log_out: 'U heeft geopende apps. Weet u zeker dat u wilt uitloggen?',
+ confirm_new_password: 'Bevestig Nieuw Wachtwoord',
+ confirm_delete_user: 'Weet u zeker dat u uw account wilt verwijderen? Al uw bestanden en gegevens worden permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt.',
+ confirm_delete_user_title: 'Account Verwijderen?',
+ confirm_session_revoke: 'Weet u zeker dat u deze sessie wilt intrekken?',
+ confirm_your_email_address: 'Bevestig Uw E-mailadres',
+ contact_us: 'Neem Contact Op',
+ contact_us_verification_required: 'U moet een geverifieerd e-mailadres hebben om dit te gebruiken.',
contain: 'Bevatten',
- continue: "Doorgaan",
+ continue: 'Doorgaan',
copy: 'Kopiëren',
- copy_link: "Kopieer koppeling",
- copying: "Kopiëren",
+ copy_link: 'Link Kopiëren',
+ copying: 'Kopiëren',
+ copying_file: 'Bestand Kopiëren %%',
cover: 'Omslag',
- create_account: "Maak Account",
- create_free_account: "Maak Gratis Account",
- create_shortcut: "Maak Snelkoppeling",
- credits: "Credits",
- current_password: "Huidig Wachtwoord",
+ create_account: 'Account Aanmaken',
+ create_free_account: 'Gratis Account Aanmaken',
+ create_shortcut: 'Snelkoppeling Maken',
+ credits: 'Credits',
+ current_password: 'Huidig Wachtwoord',
cut: 'Knippen',
- date_modified: 'Datum gewijzigd',
+ clock: 'Klok',
+ clock_visible_hide: 'Verbergen - Altijd verborgen',
+ clock_visible_show: 'Weergeven - Altijd zichtbaar',
+ clock_visible_auto: 'Auto - Standaard, alleen zichtbaar in de volledige schermmodus.',
+ close_all: 'Alles Sluiten',
+ created: 'Gemaakt',
+ date_modified: 'Datum Gewijzigd',
+ default: 'Standaard',
delete: 'Verwijderen',
- delete_account: "Account Verwijderen",
- delete_permanently: "Permanent Verwijderen",
- deploy_as_app: 'Uitrollen als applicatie',
+ delete_account: 'Account Verwijderen',
+ delete_permanently: 'Permanent Verwijderen',
+ deleting_file: 'Bestand Verwijderen %%',
+ deploy_as_app: 'Uitrollen als app',
descending: 'Aflopend',
- desktop_background_fit: "Passend",
- developers: "Ontwikkelaars",
- dir_published_as_website: `%strong% is gepubliceerd naar:`,
- disassociate_dir: "Map Ontkoppelen",
+ desktop: 'Bureaublad',
+ desktop_background_fit: 'Aanpassen',
+ developers: 'Ontwikkelaars',
+ dir_published_as_website: `%strong% is gepubliceerd op:`,
+ disable_2fa: '2FA Uitschakelen',
+ disable_2fa_confirm: 'Weet u zeker dat u 2FA wilt uitschakelen?',
+ disable_2fa_instructions: 'Voer uw wachtwoord in om 2FA uit te schakelen.',
+ disassociate_dir: 'Map Loskoppelen',
+ documents: 'Documenten',
+ dont_allow: 'Niet Toestaan',
download: 'Downloaden',
download_file: 'Bestand Downloaden',
- downloading: "Aan het downloaden",
- email: "Emailadres",
- email_or_username: "Emailadres of Gebruikersnaam",
- empty_trash: 'Prullenbak Legen',
- empty_trash_confirmation: `Weet u zeker dat u alle bestanden in de Prullenbak permanent wilt verwijderen?`,
+ downloading: 'Downloaden',
+ email: 'E-mail',
+ email_change_confirmation_sent: 'Er is een bevestigingsmail gestuurd naar uw nieuwe e-mailadres. Controleer uw inbox en volg de instructies om het proces te voltooien.',
+ email_invalid: 'E-mail is ongeldig.',
+ email_or_username: 'E-mail of Gebruikersnaam',
+ email_required: 'E-mail is verplicht.',
+ empty_trash: 'Prullenbak Leegmaken',
+ empty_trash_confirmation: `Weet u zeker dat u de items in de prullenbak permanent wilt verwijderen?`,
emptying_trash: 'Prullenbak Legen…',
- enter_password_to_confirm_delete_user: "Voer uw wachtwoord in om te bevestigen dat uw account verwijderd wordt",
- feedback: "Feedback",
- feedback_c2a: "Gebruik alstublieft het onderstaande formulier om ons uw feedback, opmerkingen en foutmeldingsrapporten te sturen.",
- feedback_sent_confirmation: "Bedankt dat u contact met ons opneemt. Als er een emailadres aan uw account is gekoppeld, hoort u zo snel mogelijk van ons terug.",
- forgot_pass_c2a: "Wachtwoord Vergeten?",
- from: "Van",
- general: "Algemeen",
- get_a_copy_of_on_puter: `Bemachtig een kopie van '%%' op Puter.com!`,
- get_copy_link: 'Krijg Kopieer Koppeling',
- hide_all_windows: "Verberg alle Vensters",
+ enable_2fa: '2FA Inschakelen',
+ end_hard: 'Hard Stoppen',
+ end_process_force_confirm: 'Weet u zeker dat u dit proces geforceerd wilt beëindigen?',
+ end_soft: 'Zacht Stoppen',
+ enlarged_qr_code: 'Vergrote QR Code',
+ enter_password_to_confirm_delete_user: 'Voer uw wachtwoord in om de verwijdering van het account te bevestigen',
+ error_message_is_missing: 'Foutbericht ontbreekt.',
+ error_unknown_cause: 'Er is een onbekende fout opgetreden.',
+ error_uploading_files: 'Bestanden uploaden mislukt',
+ favorites: 'Favorieten',
+ feedback: 'Feedback',
+ feedback_c2a: 'Gebruik het onderstaande formulier om ons uw feedback, opmerkingen en bugrapporten te sturen.',
+ feedback_sent_confirmation: 'Bedankt dat u contact met ons heeft opgenomen. Als u een e-mail heeft gekoppeld aan uw account, hoort u zo snel mogelijk van ons.',
+ fit: 'Aanpassen',
+ folder: 'Map',
+ force_quit: 'Geforceerd Stoppen',
+ forgot_pass_c2a: 'Wachtwoord vergeten?',
+ from: 'Van',
+ general: 'Algemeen',
+ get_a_copy_of_on_puter: `Krijg een kopie van '%%' op Puter.com!`,
+ get_copy_link: 'Krijg Kopieerlink',
+ hide_all_windows: 'Alle Vensters Verbergen',
+ home: 'Home',
html_document: 'HTML document',
+ hue: 'Tint',
image: 'Afbeelding',
- invite_link: "Uitnodiging Koppeling",
- item: 'Bestand',
- items_in_trash_cannot_be_renamed: `Dit bestand kan niet hernoemd worden omdat het in de Prullenbak zit. Om dit bestand te hernoemen, verplaats het eerst uit de Prullenbak.`,
+ incorrect_password: 'Onjuist wachtwoord',
+ invite_link: 'Uitnodigingslink',
+ item: 'item',
+ items_in_trash_cannot_be_renamed: `Dit item kan niet worden hernoemd omdat het in de prullenbak zit. Om dit item te hernoemen, sleept u het eerst uit de prullenbak.`,
jpeg_image: 'JPEG afbeelding',
- keep_in_taskbar: 'In Taakbalk houden',
- language: "Taal",
- license: "Licentie",
+ keep_in_taskbar: 'In Taakbalk Houden',
+ language: 'Taal',
+ license: 'Licentie',
+ lightness: 'Helderheid',
+ link_copied: 'Link gekopieerd',
loading: 'Laden',
- log_in: "Inloggen",
- log_into_another_account_anyway: 'Alsnog met ander account inloggen',
+ log_in: 'Inloggen',
+ log_into_another_account_anyway: 'Log toch in op een ander account',
log_out: 'Uitloggen',
+ looks_good: 'Ziet er goed uit!',
+ manage_sessions: 'Sessies Beheren',
+ menubar_style: 'Menubalk Stijl',
+ menubar_style_desktop: 'Bureaublad',
+ menubar_style_system: 'Systeem',
+ menubar_style_window: 'Venster',
+ mobile_device: 'Mobiel apparaat',
+ modified: 'Gewijzigd',
move: 'Verplaatsen',
- moving_file: "Aan het verplaatsen %%",
- my_websites: "Mijn Websites",
+ moving_file: 'Verplaatsen %%',
+ my_websites: 'Mijn Websites',
name: 'Naam',
name_cannot_be_empty: 'Naam kan niet leeg zijn.',
- name_cannot_contain_double_period: "Naam mag geen '..'-karakter bevatten.",
- name_cannot_contain_period: "Naam mag geen '.'-karakter bevatten.",
- name_cannot_contain_slash: "Naam mag geen '/'-karakter bevatten.",
- name_must_be_string: "Naam kan alleen een tekenreeks zijn.",
- name_too_long: `Naam kan niet langer zijn dan %% karakters.`,
+ name_cannot_contain_double_period: 'Naam mag geen dubbele punt \'..\' bevatten.',
+ name_cannot_contain_period: 'Naam mag geen punt \' . \' bevatten.',
+ name_cannot_contain_slash: 'Naam mag geen schuine streep \' / \' bevatten.',
+ name_must_be_string: 'Naam moet een alfanumerieke tekenreeks zijn.',
+ name_too_long: 'Naam mag niet langer zijn dan %% karakters.',
new: 'Nieuw',
+ new_email: 'Nieuwe E-mail',
new_folder: 'Nieuwe Map',
- new_password: "Nieuw Wachtwoord",
- new_username: "Nieuwe Gebruikersnaam",
+ new_password: 'Nieuw Wachtwoord',
+ new_username: 'Nieuwe Gebruikersnaam',
no: 'Nee',
- no_dir_associated_with_site: 'Er is geen map gekoppeld aan dit adres.',
- no_websites_published: "U heeft nog geen websites gepubliceerd.",
- ok: 'Oke',
- open: "Open",
- open_in_new_tab: "Open in Nieuw Tabblad",
- open_in_new_window: "Open in Nieuw Scherm",
- open_with: "Openen met",
- oss_code_and_content: "Open source-software en inhoud",
- password: "Wachtwoord",
- password_changed: "Wachtwoord veranderd.",
- passwords_do_not_match: '`Nieuw Wachtwoord` en `Bevestig Nieuw Wachtwoord` komen niet overeen.',
+ no_dir_associated_with_site: 'Geen map geassocieerd met dit adres.',
+ no_websites_published: 'Je hebt nog geen websites gepubliceerd. Klik met de rechtermuisknop op een map om te beginnen.',
+ ok: 'OK',
+ open: 'Openen',
+ open_in_new_tab: 'Openen in Nieuw Tabblad',
+ open_in_new_window: 'Openen in Nieuw Venster',
+ open_with: 'Openen met',
+ original_name: 'Originele Naam',
+ original_path: 'Origineel Pad',
+ oss_code_and_content: 'Open Source Software Code en Inhoud',
+ password: 'Wachtwoord',
+ password_changed: 'Wachtwoord gewijzigd',
+ password_recovery_rate_limit: 'U heeft te veel verzoeken ingediend. Wacht een paar minuten voordat u het opnieuw probeert.',
+ password_recovery_token_invalid: 'Wachtwoord hersteltoken is verlopen of ongeldig.',
+ password_recovery_unknown_error: 'Onbekende fout opgetreden bij het herstellen van het wachtwoord. Probeer het opnieuw.',
+ password_required: 'Wachtwoord is verplicht.',
+ password_strength_error: 'Wachtwoord moet minimaal 8 tekens lang zijn en minimaal één hoofdletter, één kleine letter, één cijfer en één speciaal teken bevatten.',
+ passwords_do_not_match: 'Wachtwoorden komen niet overeen.',
paste: 'Plakken',
- paste_into_folder: "Plakken in Map",
- pick_name_for_website: "Kies een naam voor uw website:",
- picture: "Foto",
- powered_by_puter_js: `Aangedreven door {{link=docs}}Puter.js{{/link}}`,
- preparing: "Voorbereiden...",
- preparing_for_upload: "Upload voorbereiden...",
- proceed_to_login: 'Doorgaan naar Inloggen',
- properties: "Eigenschappen",
- privacy: "Privacy",
- proceed_with_account_deletion: "Doorgaan met Account Verwijderen",
- publish: "Publiceren",
- publish_as_website: 'Publiceren als website',
- puter_description: `Puter is een persoonlijke cloud waarin privacy centraal staat en waarmee u al uw bestanden, apps en games in één veilige plek kunt bewaren die altijd en overal toegankelijk.`,
- recent: "Recent",
- recover_password: "Wachtwoord herstellen",
- refer_friends_c2a: "Krijg 1 GB voor elke vriend die een account maakt en bevestigt bij Puter. uw vriend ontvangt ook 1 GB!",
- refer_friends_social_media_c2a: `Krijg 1 GB gratig opslag op Puter.com!`,
- refresh: 'Verversen',
- release_address_confirmation: `Weet u zeker dat u dit adres wilt vrijgeven?`,
- remove_from_taskbar:'Losmaken van Taakbalk',
+ paste_into_folder: 'Plakken in Map',
+ path: 'Pad',
+ personalization: 'Personalisatie',
+ pick_name_for_website: 'Kies een naam voor uw website',
+ picture: 'Afbeelding',
+ pictures: 'Afbeeldingen',
+ plural_suffix: 'en',
+ powered_by_puter_js: 'Aangedreven door {{link=docs}}Puter.js{{/link}}',
+ preparing: 'Voorbereiden...',
+ preparing_for_upload: 'Voorbereiden voor upload...',
+ print: 'Afdrukken',
+ privacy: 'Privacy',
+ proceed_to_login: 'Doorgaan naar inloggen',
+ proceed_with_account_deletion: 'Doorgaan met accountverwijdering',
+ process_status_initializing: 'Initialiseren',
+ process_status_running: 'Bezig',
+ process_type_app: 'App',
+ process_type_init: 'Init',
+ process_type_ui: 'UI',
+ properties: 'Eigenschappen',
+ public: 'Openbaar',
+ publish: 'Publiceren',
+ publish_as_website: 'Publiceer als website',
+ puter_description: `Puter is een privacy-first persoonlijke cloud om al je bestanden, apps en games op één veilige plek te bewaren, overal en altijd toegankelijk.`,
+ reading_file: 'Lezen %strong%',
+ recent: 'Recent',
+ recommended: 'Aanbevolen',
+ recover_password: 'Wachtwoord herstellen',
+ refer_friends_c2a: 'Krijg 1 GB voor elke vriend die een account aanmaakt en bevestigt op Puter. Je vriend krijgt ook 1 GB!',
+ refer_friends_social_media_c2a: `Krijg 1 GB gratis opslagruimte op Puter.com!`,
+ refresh: 'Vernieuwen',
+ release_address_confirmation: `Weet je zeker dat je dit adres wilt vrijgeven?`,
+ remove_from_taskbar: 'Verwijderen van taakbalk',
rename: 'Hernoemen',
repeat: 'Herhalen',
replace: 'Vervangen',
- replace_all: 'Alles Vervangen',
- resend_confirmation_code: "Confirmatie Code opnieuw verzenden",
- restore: "Herstellen",
+ replace_all: 'Alles vervangen',
+ resend_confirmation_code: 'Bevestigingscode opnieuw verzenden',
+ reset_colors: 'Kleuren resetten',
+ restart_puter_confirm: 'Weet je zeker dat je Puter wilt herstarten?',
+ restore: 'Herstellen',
+ save: 'Opslaan',
+ saturation: 'Verzadiging',
save_account: 'Account opslaan',
- save_account_to_get_copy_link: "Maak alstublieft een account om door te gaan.",
- save_account_to_publish: 'Maak alstublieft een account om door te gaan.',
- save_session: 'Sessie Opslaan',
- save_session_c2a: 'Maak een account aan om uw huidige sessie op te slaan en te voorkomen dat u uw werk kwijtraakt.',
- scan_qr_c2a: 'Scan de onderstaande QR-code om op deze sessie in te loggen op een ander apparaat',
- select: "Selecteren",
+ save_account_to_get_copy_link: 'Maak een account aan om verder te gaan.',
+ save_account_to_publish: 'Maak een account aan om verder te gaan.',
+ save_session: 'Sessie opslaan',
+ save_session_c2a: 'Maak een account aan om je huidige sessie op te slaan en verlies van werk te voorkomen.',
+ scan_qr_c2a: 'Scan de onderstaande code\nom in te loggen op deze sessie vanaf andere apparaten',
+ scan_qr_2fa: 'Scan de QR-code met je authenticator-app',
+ scan_qr_generic: 'Scan deze QR-code met je telefoon of een ander apparaat',
+ search: 'Zoeken',
+ seconds: 'seconden',
+ security: 'Beveiliging',
+ select: 'Selecteren',
selected: 'geselecteerd',
select_color: 'Selecteer kleur…',
- send: "Versturen",
- send_password_recovery_email: "Verstuur Wachtwoord Herstel Email",
- session_saved: "Bedankt voor het maken van een account. Deze sessie is nu opgeslagen.",
+ sessions: 'Sessies',
+ send: 'Verzenden',
+ send_password_recovery_email: 'Wachtwoordherstelsmail verzenden',
+ session_saved: 'Bedankt voor het aanmaken van een account. Deze sessie is opgeslagen.',
settings: 'Instellingen',
- set_new_password: "Stel een nieuw wachtwoord in",
- share_to: "Delen naar",
- show_all_windows: "Toon alle vensters",
- show_hidden: 'Toon Verborgen',
- sign_in_with_puter: "Inloggen met Puter",
- sign_up: "Aanmelden",
- signing_in: "Aan het inloggen…",
+ set_new_password: 'Nieuw wachtwoord instellen',
+ share: 'Delen',
+ share_to: 'Delen met',
+ share_with: 'Delen met:',
+ shortcut_to: 'Snelkoppeling naar',
+ show_all_windows: 'Toon alle vensters',
+ show_hidden: 'Toon verborgen',
+ sign_in_with_puter: 'Inloggen met Puter',
+ sign_up: 'Aanmelden',
+ signing_in: 'Inloggen…',
size: 'Grootte',
skip: 'Overslaan',
+ something_went_wrong: 'Er is iets misgegaan.',
sort_by: 'Sorteren op',
start: 'Start',
- status: "Status",
- storage_usage: "Opslaggebruik",
- taking_longer_than_usual: 'Het duurt iets langer dan normaal. Even geduld aub...',
- terms: "Voorwaarden",
- text_document: 'Tekst document',
- tos_fineprint: `Door te klikken op 'Maak Gratis Account' gaat u akkoord met Puter's {{link=terms}}Gebruiksvoorwaarden{{/link}} en {{link=privacy}}Privacybeleid{{/link}}.`,
+ status: 'Status',
+ storage_usage: 'Opslaggebruik',
+ storage_puter_used: 'gebruikt door Puter',
+ taking_longer_than_usual: 'Het duurt langer dan normaal. Even geduld alstublieft...',
+ task_manager: 'Taakbeheer',
+ taskmgr_header_name: 'Naam',
+ taskmgr_header_status: 'Status',
+ taskmgr_header_type: 'Type',
+ terms: 'Voorwaarden',
+ text_document: 'Tekstdocument',
+ tos_fineprint: `Door op 'Gratis Account Aanmaken' te klikken, ga je akkoord met de {{link=terms}}Servicevoorwaarden{{/link}} en de {{link=privacy}}Privacybeleid{{/link}} van Puter.`,
+ transparency: 'Transparantie',
trash: 'Prullenbak',
+ two_factor: 'Tweestapsverificatie',
+ two_factor_disabled: '2FA Uitgeschakeld',
+ two_factor_enabled: '2FA Ingeschakeld',
type: 'Type',
- type_confirm_to_delete_account: "Type 'bevestig' om uw account te verwijderen.",
+ type_confirm_to_delete_account: 'Typ \'bevestigen\' om je account te verwijderen.',
+ ui_colors: 'UI Kleuren',
+ ui_manage_sessions: 'Sessiebeheer',
+ ui_revoke: 'Intrekken',
undo: 'Ongedaan maken',
unlimited: 'Onbeperkt',
- unzip: "Uitpakken",
+ unzip: 'Uitpakken',
upload: 'Uploaden',
- upload_here: 'Hier Uploaden',
+ upload_here: 'Hier uploaden',
usage: 'Gebruik',
- username: "Gebruikersnaam",
- username_changed: 'Gebruikersnaam succesvol aangepast.',
- versions: "Versies",
+ username: 'Gebruikersnaam',
+ username_changed: 'Gebruikersnaam succesvol bijgewerkt.',
+ username_required: 'Gebruikersnaam is vereist.',
+ versions: 'Versies',
+ videos: 'Video\'s',
+ visibility: 'Zichtbaarheid',
yes: 'Ja',
- yes_release_it: 'Ja, Geef het vrij',
- you_have_been_referred_to_puter_by_a_friend: "Je bent door een vriend naar Puter verwezen!",
- zip: "Inpakken",
+ yes_release_it: 'Ja, vrijgeven',
+ you_have_been_referred_to_puter_by_a_friend: 'Je bent door een vriend doorverwezen naar Puter!',
+ zip: 'Zip',
+ zipping_file: 'Bestand inpakken %strong%',
+
+ // === 2FA Setup ===
+ setup2fa_1_step_heading: 'Open je authenticator-app',
+ setup2fa_1_instructions: `
+ Je kunt elke authenticator-app gebruiken die het Time-based One-Time Password (TOTP) protocol ondersteunt.
+ Er zijn veel opties om uit te kiezen, maar als je twijfelt,
+ Authy
+ is een goede keuze voor Android en iOS.
+ `,
+ setup2fa_2_step_heading: 'Scan de QR-code',
+ setup2fa_3_step_heading: 'Voer de 6-cijferige code in',
+ setup2fa_4_step_heading: 'Kopieer je herstelcodes',
+ setup2fa_4_instructions: `
+ Deze herstelcodes zijn de enige manier om toegang te krijgen tot je account als je je telefoon kwijtraakt of je authenticator-app niet kunt gebruiken.
+ Zorg ervoor dat je ze op een veilige plaats bewaart.
+ `,
+ setup2fa_5_step_heading: 'Bevestig 2FA-instelling',
+ setup2fa_5_confirmation_1: 'Ik heb mijn herstelcodes op een veilige plaats opgeslagen',
+ setup2fa_5_confirmation_2: 'Ik ben klaar om 2FA in te schakelen',
+ setup2fa_5_button: '2FA inschakelen',
+
+ // === 2FA Login ===
+ login2fa_otp_title: 'Voer 2FA-code in',
+ login2fa_otp_instructions: 'Voer de 6-cijferige code in uit je authenticator-app.',
+ login2fa_recovery_title: 'Voer een herstelcode in',
+ login2fa_recovery_instructions: 'Voer een van je herstelcodes in om toegang te krijgen tot je account.',
+ login2fa_use_recovery_code: 'Gebruik een herstelcode',
+ login2fa_recovery_back: 'Terug',
+ login2fa_recovery_placeholder: 'XXXXXXXX',
}
};
diff --git a/tools/run-selfhosted.js b/tools/run-selfhosted.js
index a659656a1..ec2985bca 100644
--- a/tools/run-selfhosted.js
+++ b/tools/run-selfhosted.js
@@ -83,7 +83,8 @@ const main = async () => {
CoreModule,
DatabaseModule,
LocalDiskStorageModule,
- SelfHostedModule
+ SelfHostedModule,
+ TestDriversModule,
} = (await import('@heyputer/backend')).default;
const k = new Kernel({
@@ -93,6 +94,7 @@ const main = async () => {
k.add_module(new DatabaseModule());
k.add_module(new LocalDiskStorageModule());
k.add_module(new SelfHostedModule());
+ k.add_module(new TestDriversModule());
k.boot();
};