Add puter.js

This commit is contained in:
KernelDeimos 2024-04-11 19:07:00 -04:00
parent fc5025a2a8
commit b8e66cada9
51 changed files with 12907 additions and 3 deletions

View File

@ -9,6 +9,30 @@ class SelfhostedModule extends AdvancedBase {
const ComplainAboutVersionsService = require('./services/ComplainAboutVersionsService');
services.registerService('complain-about-versions', ComplainAboutVersionsService);
const DevWatcherService = require('./services/DevWatcherService');
const path_ = require('path');
services.registerService('__dev-watcher', DevWatcherService, {
root: path_.resolve(__dirname, '../../../'),
commands: [
{
name: 'puter.js:webpack-watch',
directory: 'packages/puter-dot-js',
command: 'npm',
args: ['run', 'start-webpack'],
},
],
});
const ServeStaticFilesService = require("./services/ServceStaticFilesService");
services.registerService('__serve-puterjs', ServeStaticFilesService, {
directories: [
{
prefix: '/sdk',
path: path_.resolve(__dirname, '../../../packages/puter-dot-js/dist'),
},
],
});
}
}

View File

@ -0,0 +1,93 @@
const BaseService = require("./BaseService");
class ProxyLogger {
constructor (log) {
this.log = log;
}
attach (stream) {
let buffer = '';
stream.on('data', (chunk) => {
buffer += chunk.toString();
let lineEndIndex = buffer.indexOf('\n');
while (lineEndIndex !== -1) {
const line = buffer.substring(0, lineEndIndex);
this.log(line);
buffer = buffer.substring(lineEndIndex + 1);
lineEndIndex = buffer.indexOf('\n');
}
});
stream.on('end', () => {
if (buffer.length) {
this.log(buffer);
}
});
}
}
/**
* @description
* This service is used to run webpack watchers.
*/
class DevWatcherService extends BaseService {
static MODULES = {
path: require('path'),
spawn: require('child_process').spawn,
};
_construct () {
this.instances = [];
}
async _init (args) {
const { root, commands } = args;
process.on('exit', () => {
this.exit_all_();
})
for ( const entry of commands ) {
const { name, directory, command, args } = entry;
const fullpath = this.modules.path.join(
root, directory);
this.start_({ name, fullpath, command, args });
}
}
log_ (name, isErr, line) {
let txt = `[${name}:`;
txt += isErr
? `\x1B[31;1merr\x1B[0m`
: `\x1B[32;1mout\x1B[0m`;
txt += '] ' + line;
this.log.info(txt);
}
async start_ ({ name, fullpath, command, args }) {
this.log.info(`Starting ${name} in ${fullpath}`);
const proc = this.modules.spawn(command, args, {
shell: true,
env: process.env,
cwd: fullpath,
});
this.instances.push({
name, proc,
});
const out = new ProxyLogger((line) => this.log_(name, false, line));
out.attach(proc.stdout);
const err = new ProxyLogger((line) => this.log_(name, true, line));
err.attach(proc.stderr);
proc.on('exit', () => {
this.log.info(`[${name}:exit] Process exited (${proc.exitCode})`);
this.instances = this.instances.filter((inst) => inst.proc !== proc);
})
}
async exit_all_ () {
for ( const { proc } of this.instances ) {
proc.kill();
}
}
};
module.exports = DevWatcherService;

View File

@ -0,0 +1,17 @@
const BaseService = require("./BaseService");
class ServeStaticFilesService extends BaseService {
async _init (args) {
this.directories = args.directories;
}
async ['__on_install.routes'] () {
const { app } = this.services.get('web-server');
for ( const { prefix, path } of this.directories ) {
app.use(prefix, require('express').static(path));
}
}
}
module.exports = ServeStaticFilesService;

View File

@ -0,0 +1,19 @@
let memoized_common_template_vars_ = null;
const get_common_template_vars = () => {
const path_ = require('path');
if ( memoized_common_template_vars_ !== null ) {
return memoized_common_template_vars_;
}
const code_root = path_.resolve(__dirname, '../../');
memoized_common_template_vars_ = {
code_root,
};
return memoized_common_template_vars_;
}
module.exports = {
get_common_template_vars,
};

137
packages/puter-dot-js/.gitignore vendored Normal file
View File

@ -0,0 +1,137 @@
# MAC OS hidden directory settings file
.DS_Store
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env.production
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# End of https://www.toptal.com/developers/gitignore/api/node
*.zip
*.pem
.DS_Store
./build
build
# config file
src/config.js
ssl
ssl/

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Puter Technologies Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,55 @@
<h3 align="center">Puter.js</h3>
<h4 align="center">The official JavaScript SDK for Puter.com. Cloud and AI features right from your frontend code!</h4>
<p align="center">
<a href="https://docs.puter.com/playground/"><strong>« LIVE DEMO »</strong></a>
<br />
<br />
<a href="https://docs.puter.com" target="_blank">Docs</a>
·
<a href="https://puter.com">Puter.com</a>
·
<a href="https://discord.com/invite/PQcx7Teh8u">Discord</a>
·
<a href="https://reddit.com/r/puter">Reddit</a>
·
<a href="https://twitter.com/HeyPuter">X (Twitter)</a>
</p>
## Installation
```
git clone https://github.com/HeyPuter/puter.js.git
cd puter.js
npm install
```
## Run development server
```
npm start
```
## Build
```
npm run build
```
### Example
Make sure the development server is running.
```html
<html>
<body>
<script src="http://127.0.0.1:8080/dist/puter.dev.js"></script>
<script>
// Loading ...
puter.print(`Loading...`);
// Chat with GPT-3.5 Turbo
puter.ai.chat(`What color was Napoleon's white horse?`).then((response) => {
puter.print(response);
});
</script>
</body>
</html>
```

View File

@ -0,0 +1,12 @@
all UI function calls from puter should be in the form `puter.ui.<function>`
e.g. `puter.showOpenFilePicker` becomes `puter.ui.showOpenFilePicker`
puter.FileSystem.<function> -> puter.fs.<function>
puter.Router.<function> -> puter.router.<function>
puter.Apps.<function> -> puter.apps.<function>
puter.setItem -> puter.kv.set
puter.getItem -> puter.kv.get
puter.removeItem -> puter.kv.del
puter.createCloudItem(...) -> new puter.CloudItem(...)
puter.router.* -> puter.hosting.*

1738
packages/puter-dot-js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
{
"name": "puter",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start-server": "npx http-server --cors -c-1",
"start-webpack": "export NODE_OPTIONS=--openssl-legacy-provider && webpack ./src/index.js --output-filename puter.js && webpack ./src/index.js --output-filename puter.dev.js --watch --devtool source-map",
"start": "concurrently \"npm run start-server\" \"npm run start-webpack\"",
"build": "export NODE_OPTIONS=--openssl-legacy-provider && webpack ./src/index.js --output-filename puter.js && { echo \"// Copyright 2024 Puter Technologies Inc. All rights reserved.\"; echo \"// Generated on $(date '+%Y-%m-%d %H:%M')\n\"; cat ./dist/puter.js; } > temp && mv temp ./dist/puter.js"
},
"keywords": [],
"author": "",
"license": "Apache-2.0",
"devDependencies": {
"concurrently": "^8.2.2",
"webpack-cli": "^5.1.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -0,0 +1,358 @@
import OS from './modules/OS.js';
import FileSystem from './modules/FileSystem/index.js';
import Hosting from './modules/Hosting.js';
import Apps from './modules/Apps.js';
import UI from './modules/UI.js';
import KV from './modules/KV.js';
import AI from './modules/AI.js';
import Auth from './modules/Auth.js';
import FSItem from './modules/FSItem.js';
import * as utils from './lib/utils.js';
import path from './lib/path.js';
window.puter = (function() {
'use strict';
class Puter{
// The environment that the SDK is running in. Can be 'gui', 'app' or 'web'.
// 'gui' means the SDK is running in the Puter GUI, i.e. Puter.com.
// 'app' means the SDK is running as a Puter app, i.e. within an iframe in the Puter GUI.
// 'web' means the SDK is running in a 3rd-party website.
env;
defaultAPIOrigin = 'https://api.puter.com';
defaultGUIOrigin = 'https://puter.com';
// An optional callback when the user is authenticated. This can be set by the app using the SDK.
onAuth;
/**
* State object to keep track of the authentication request status.
* This is used to prevent multiple authentication popups from showing up by different parts of the app.
*/
puterAuthState = {
isPromptOpen: false,
authGranted: null,
resolver: null
};
// Holds the unique app instance ID that is provided by the host environment
appInstanceID;
// Holds the unique app instance ID for the parent (if any), which is provided by the host environment
parentInstanceID;
// Expose the FSItem class
static FSItem = FSItem;
// Event handling properties
eventHandlers = {};
// --------------------------------------------
// Constructor
// --------------------------------------------
constructor(options) {
options = options ?? {};
// Holds the query parameters found in the current URL
let URLParams = new URLSearchParams(window.location.search);
// Figure out the environment in which the SDK is running
if (URLParams.has('puter.app_instance_id'))
this.env = 'app';
else if(window.puter_gui_enabled === true)
this.env = 'gui';
else
this.env = 'web';
// there are some specific situations where puter is definitely loaded in GUI mode
// we're going to check for those situations here so that we don't break anything unintentionally
// if navigator URL's hostname is 'puter.com'
if(window.location.hostname === 'puter.com'){
this.env = 'gui';
}
// Get the 'args' from the URL. This is used to pass arguments to the app.
if(URLParams.has('puter.args')){
this.args = JSON.parse(decodeURIComponent(URLParams.get('puter.args')));
}else{
this.args = {};
}
// Try to extract appInstanceID from the URL. appInstanceID is included in every messaage
// sent to the host environment. This is used to help host environment identify the app
// instance that sent the message and communicate back to it.
if(URLParams.has('puter.app_instance_id')){
this.appInstanceID = decodeURIComponent(URLParams.get('puter.app_instance_id'));
}
// Try to extract parentInstanceID from the URL. If another app launched this app instance, parentInstanceID
// holds its instance ID, and is used to communicate with that parent app.
if(URLParams.has('puter.parent_instance_id')){
this.parentInstanceID = decodeURIComponent(URLParams.get('puter.parent_instance_id'));
}
// Try to extract `puter.app.id` from the URL. `puter.app.id` is the unique ID of the app.
// App ID is useful for identifying the app when communicating with the Puter API, among other things.
if(URLParams.has('puter.app.id')){
this.appID = decodeURIComponent(URLParams.get('puter.app.id'));
}
// Construct this App's AppData path based on the appID. AppData path is used to store files that are specific to this app.
// The default AppData path is `~/AppData/<appID>`.
if(this.appID){
this.appDataPath = `~/AppData/${this.appID}`;
}
// Construct APIOrigin from the URL. APIOrigin is used to build the URLs for the Puter API endpoints.
// The default APIOrigin is https://api.puter.com. However, if the URL contains a `puter.api_origin` query parameter,
// then that value is used as the APIOrigin. If the URL contains a `puter.domain` query parameter, then the APIOrigin
// is constructed as `https://api.<puter.domain>`.
this.APIOrigin = this.defaultAPIOrigin;
if(URLParams.has('puter.api_origin')){
this.APIOrigin = decodeURIComponent(URLParams.get('puter.api_origin'));
}else if(URLParams.has('puter.domain')){
this.APIOrigin = 'https://api.' + URLParams.get('puter.domain');
}
// The SDK is running in the Puter GUI (i.e. 'gui')
if(this.env === 'gui'){
this.authToken = window.auth_token;
// initialize submodules
this.initSubmodules();
}
// Loaded in an iframe in the Puter GUI (i.e. 'app')
// When SDK is loaded in App mode the initiation process should start when the DOM is ready
else if (this.env === 'app') {
this.authToken = decodeURIComponent(URLParams.get('puter.auth.token'));
// initialize submodules
this.initSubmodules();
// If the authToken is already set in localStorage, then we don't need to show the dialog
try {
if(localStorage.getItem('puter.auth.token')){
this.setAuthToken(localStorage.getItem('puter.auth.token'));
}
// if appID is already set in localStorage, then we don't need to show the dialog
if(localStorage.getItem('puter.app.id')){
this.setAppID(localStorage.getItem('puter.app.id'));
}
} catch (error) {
// Handle the error here
console.error('Error accessing localStorage:', error);
}
}
// SDK was loaded in a 3rd-party website.
// When SDK is loaded in GUI the initiation process should start when the DOM is ready. This is because
// the SDK needs to show a dialog to the user to ask for permission to access their Puter account.
else if(this.env === 'web') {
// initialize submodules
this.initSubmodules();
try{
// If the authToken is already set in localStorage, then we don't need to show the dialog
if(localStorage.getItem('puter.auth.token')){
this.setAuthToken(localStorage.getItem('puter.auth.token'));
}
// if appID is already set in localStorage, then we don't need to show the dialog
if(localStorage.getItem('puter.app.id')){
this.setAppID(localStorage.getItem('puter.app.id'));
}
} catch (error) {
// Handle the error here
console.error('Error accessing localStorage:', error);
}
}
}
// Initialize submodules
initSubmodules = function(){
// Auth
this.auth = new Auth(this.authToken, this.APIOrigin, this.appID, this.env);
// OS
this.os = new OS(this.authToken, this.APIOrigin, this.appID, this.env);
// FileSystem
this.fs = new FileSystem(this.authToken, this.APIOrigin, this.appID, this.env);
// UI
this.ui = new UI(this.appInstanceID, this.parentInstanceID, this.appID, this.env);
// Hosting
this.hosting = new Hosting(this.authToken, this.APIOrigin, this.appID, this.env);
// Apps
this.apps = new Apps(this.authToken, this.APIOrigin, this.appID, this.env);
// AI
this.ai = new AI(this.authToken, this.APIOrigin, this.appID, this.env);
// Key-Value Store
this.kv = new KV(this.authToken, this.APIOrigin, this.appID, this.env);
// Path
this.path = path;
}
updateSubmodules() {
// Update submodules with new auth token and API origin
[this.os, this.fs, this.hosting, this.apps, this.ai, this.kv].forEach(module => {
if(!module) return;
module.setAuthToken(this.authToken);
module.setAPIOrigin(this.APIOrigin);
});
}
setAppID = function (appID) {
// save to localStorage
try{
localStorage.setItem('puter.app.id', appID);
} catch (error) {
// Handle the error here
console.error('Error accessing localStorage:', error);
}
this.appID = appID;
}
setAuthToken = function (authToken) {
this.authToken = authToken;
// If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage
if(this.env === 'web' || this.env === 'app'){
try{
localStorage.setItem('puter.auth.token', authToken);
} catch (error) {
// Handle the error here
console.error('Error accessing localStorage:', error);
}
}
// reinitialize submodules
this.updateSubmodules();
}
setAPIOrigin = function (APIOrigin) {
this.APIOrigin = APIOrigin;
// reinitialize submodules
this.updateSubmodules();
}
resetAuthToken = function () {
this.authToken = null;
// If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage
if(this.env === 'web' || this.env === 'app'){
try{
localStorage.removeItem('puter.auth.token');
} catch (error) {
// Handle the error here
console.error('Error accessing localStorage:', error);
}
}
// reinitialize submodules
this.updateSubmodules();
}
exit = function() {
window.parent.postMessage({
msg: "exit",
appInstanceID: this.appInstanceID,
}, '*');
}
/**
* A function that generates a domain-safe name by combining a random adjective, a random noun, and a random number (between 0 and 9999).
* The result is returned as a string with components separated by hyphens.
* It is useful when you need to create unique identifiers that are also human-friendly.
*
* @param {string} [separateWith='-'] - The character to use to separate the components of the generated name.
* @returns {string} A unique, hyphen-separated string comprising of an adjective, a noun, and a number.
*
*/
randName = function(separateWith = '-'){
const first_adj = ['helpful','sensible', 'loyal', 'honest', 'clever', 'capable','calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy',
'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite',
'quiet', 'relaxed', 'silly', 'victorious', 'witty', 'young', 'zealous', 'strong', 'brave', 'agile', 'bold'];
const nouns = ['street', 'roof', 'floor', 'tv', 'idea', 'morning', 'game', 'wheel', 'shoe', 'bag', 'clock', 'pencil', 'pen',
'magnet', 'chair', 'table', 'house', 'dog', 'room', 'book', 'car', 'cat', 'tree',
'flower', 'bird', 'fish', 'sun', 'moon', 'star', 'cloud', 'rain', 'snow', 'wind', 'mountain',
'river', 'lake', 'sea', 'ocean', 'island', 'bridge', 'road', 'train', 'plane', 'ship', 'bicycle',
'horse', 'elephant', 'lion', 'tiger', 'bear', 'zebra', 'giraffe', 'monkey', 'snake', 'rabbit', 'duck',
'goose', 'penguin', 'frog', 'crab', 'shrimp', 'whale', 'octopus', 'spider', 'ant', 'bee', 'butterfly', 'dragonfly',
'ladybug', 'snail', 'camel', 'kangaroo', 'koala', 'panda', 'piglet', 'sheep', 'wolf', 'fox', 'deer', 'mouse', 'seal',
'chicken', 'cow', 'dinosaur', 'puppy', 'kitten', 'circle', 'square', 'garden', 'otter', 'bunny', 'meerkat', 'harp']
// return a random combination of first_adj + noun + number (between 0 and 9999)
// e.g. clever-idea-123
return first_adj[Math.floor(Math.random() * first_adj.length)] + separateWith + nouns[Math.floor(Math.random() * nouns.length)] + separateWith + Math.floor(Math.random() * 10000);
}
getUser = function(...args){
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
success: args[0],
error: args[1],
};
}
return new Promise((resolve, reject) => {
const xhr = utils.initXhr('/whoami', this.APIOrigin, this.authToken, 'get');
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
xhr.send();
})
}
print = function(...args){
for(let arg of args){
document.getElementsByTagName('body')[0].append(arg);
}
}
}
// Create a new Puter object and return it
const puterobj = new Puter();
// Return the Puter object
return puterobj;
}());
window.addEventListener('message', async (event) => {
// if the message is not from Puter, then ignore it
if(event.origin !== puter.defaultGUIOrigin) return;
if(event.data.msg && event.data.msg === 'requestOrigin'){
event.source.postMessage({
msg: "originResponse",
}, '*');
}
else if (event.data.msg === 'puter.token') {
// puterDialog.close();
// Set the authToken property
puter.setAuthToken(event.data.token);
// update appID
puter.setAppID(event.data.app_uid);
// Remove the event listener to avoid memory leaks
// window.removeEventListener('message', messageListener);
puter.puterAuthState.authGranted = true;
// Resolve the promise
// resolve();
// Call onAuth callback
if(puter.onAuth && typeof puter.onAuth === 'function'){
puter.getUser().then((user) => {
puter.onAuth(user)
});
}
puter.puterAuthState.isPromptOpen = false;
// Resolve or reject any waiting promises.
if (puter.puterAuthState.resolver) {
if (puter.puterAuthState.authGranted) {
puter.puterAuthState.resolver.resolve();
} else {
puter.puterAuthState.resolver.reject();
}
puter.puterAuthState.resolver = null;
};
}
})

View File

@ -0,0 +1,49 @@
export default class EventListener {
// Array of all supported event names.
#eventNames;
// Map of eventName -> array of listeners
#eventListeners;
constructor(eventNames) {
this.#eventNames = eventNames;
this.#eventListeners = (() => {
const map = new Map();
for (let eventName of this.#eventNames) {
map[eventName] = [];
}
return map;
})();
}
emit(eventName, data) {
if (!this.#eventNames.includes(eventName)) {
console.error(`Event name '${eventName}' not supported`);
return;
}
this.#eventListeners[eventName].forEach((listener) => {
listener(data);
});
}
on(eventName, callback) {
if (!this.#eventNames.includes(eventName)) {
console.error(`Event name '${eventName}' not supported`);
return;
}
this.#eventListeners[eventName].push(callback);
}
off(eventName, callback) {
if (!this.#eventNames.includes(eventName)) {
console.error(`Event name '${eventName}' not supported`);
return;
}
const listeners = this.#eventListeners[eventName];
const index = listeners.indexOf(callback)
if (index !== -1) {
listeners.splice(index, 1);
}
}
}

View File

@ -0,0 +1,509 @@
// import {cwd} from './env.js'
let cwd;
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
//'use strict';
const
CHAR_UPPERCASE_A = 65,
CHAR_LOWERCASE_A = 97,
CHAR_UPPERCASE_Z = 90,
CHAR_LOWERCASE_Z = 122,
CHAR_DOT = 46,
CHAR_FORWARD_SLASH = 47,
CHAR_BACKWARD_SLASH = 92,
CHAR_COLON = 58,
CHAR_QUESTION_MARK = 63;
function isPathSeparator(code) {
return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH;
}
function isPosixPathSeparator(code) {
return code === CHAR_FORWARD_SLASH;
}
// Resolves . and .. elements in a path with directory names
function normalizeString(path, allowAboveRoot, separator, isPathSeparator) {
let res = '';
let lastSegmentLength = 0;
let lastSlash = -1;
let dots = 0;
let code = 0;
for (let i = 0; i <= path.length; ++i) {
if (i < path.length)
code = path.charCodeAt(i);
else if (isPathSeparator(code))
break;
else
code = CHAR_FORWARD_SLASH;
if (isPathSeparator(code)) {
if (lastSlash === i - 1 || dots === 1) {
// NOOP
} else if (dots === 2) {
if (res.length < 2 || lastSegmentLength !== 2 ||
res.charCodeAt( res.length - 1) !== CHAR_DOT ||
res.charCodeAt(res.length - 2) !== CHAR_DOT) {
if (res.length > 2) {
const lastSlashIndex = res.lastIndexOf(separator);
if (lastSlashIndex === -1) {
res = '';
lastSegmentLength = 0;
} else {
res = res.slice(0, lastSlashIndex);
lastSegmentLength =
res.length - 1 - res.lastIndexOf(res, separator);
}
lastSlash = i;
dots = 0;
continue;
} else if (res.length !== 0) {
res = '';
lastSegmentLength = 0;
lastSlash = i;
dots = 0;
continue;
}
}
if (allowAboveRoot) {
res += res.length > 0 ? `${separator}..` : '..';
lastSegmentLength = 2;
}
} else {
if (res.length > 0)
res += `${separator}${path.slice(lastSlash + 1, i)}`;
else
res = path.slice(lastSlash + 1, i);
lastSegmentLength = i - lastSlash - 1;
}
lastSlash = i;
dots = 0;
} else if (code === CHAR_DOT && dots !== -1) {
++dots;
} else {
dots = -1;
}
}
return res;
}
const path = {
// path.resolve([from ...], to)
resolve(...args) {
let resolvedPath = '';
let resolvedAbsolute = false;
for (let i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) {
// orig const path = i >= 0 ? args[i] : posixCwd();
const path = i >= 0 ? args[i] : (cwd !== undefined ? cwd : '/');
// const path = i >= 0 ? args[i] : '/';
// Skip empty entries
if (path.length === 0) {
continue;
}
resolvedPath = `${path}/${resolvedPath}`;
resolvedAbsolute =
path.charCodeAt(0) === CHAR_FORWARD_SLASH;
}
// At this point the path should be resolved to a full absolute path, but
// handle relative paths to be safe (might happen when process.cwd() fails)
// Normalize the path
resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute, '/',
isPosixPathSeparator);
if (resolvedAbsolute) {
return `/${resolvedPath}`;
}
return resolvedPath.length > 0 ? resolvedPath : '.';
},
normalize(path) {
if (path.length === 0)
return '.';
const isAbsolute =
path.charCodeAt(0) === CHAR_FORWARD_SLASH;
const trailingSeparator =
path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH;
// Normalize the path
path = normalizeString(path, !isAbsolute, '/', isPosixPathSeparator);
if (path.length === 0) {
if (isAbsolute)
return '/';
return trailingSeparator ? './' : '.';
}
if (trailingSeparator)
path += '/';
return isAbsolute ? `/${path}` : path;
},
isAbsolute(path) {
return path.length > 0 &&
path.charCodeAt(0) === CHAR_FORWARD_SLASH;
},
join(...args) {
if (args.length === 0)
return '.';
let joined;
for (let i = 0; i < args.length; ++i) {
const arg = args[i];
if (arg.length > 0) {
if (joined === undefined)
joined = arg;
else
joined += `/${arg}`;
}
}
if (joined === undefined)
return '.';
return path.normalize(joined);
},
relative(from, to) {
if (from === to)
return '';
// Trim leading forward slashes.
from = path.resolve(from);
to = path.resolve(to);
if (from === to)
return '';
const fromStart = 1;
const fromEnd = from.length;
const fromLen = fromEnd - fromStart;
const toStart = 1;
const toLen = to.length - toStart;
// Compare paths to find the longest common path from root
const length = (fromLen < toLen ? fromLen : toLen);
let lastCommonSep = -1;
let i = 0;
for (; i < length; i++) {
const fromCode = from.charCodeAt(fromStart + i);
if (fromCode !== to.charCodeAt(toStart + i))
break;
else if (fromCode === CHAR_FORWARD_SLASH)
lastCommonSep = i;
}
if (i === length) {
if (toLen > length) {
if (to.charCodeAt(toStart + i) === CHAR_FORWARD_SLASH) {
// We get here if `from` is the exact base path for `to`.
// For example: from='/foo/bar'; to='/foo/bar/baz'
return to.slice(toStart + i + 1);
}
if (i === 0) {
// We get here if `from` is the root
// For example: from='/'; to='/foo'
return to.slice(toStart + i);
}
} else if (fromLen > length) {
if (from.charCodeAt(fromStart + i) ===
CHAR_FORWARD_SLASH) {
// We get here if `to` is the exact base path for `from`.
// For example: from='/foo/bar/baz'; to='/foo/bar'
lastCommonSep = i;
} else if (i === 0) {
// We get here if `to` is the root.
// For example: from='/foo/bar'; to='/'
lastCommonSep = 0;
}
}
}
let out = '';
// Generate the relative path based on the path difference between `to`
// and `from`.
for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
if (i === fromEnd ||
from.charCodeAt(i) === CHAR_FORWARD_SLASH) {
out += out.length === 0 ? '..' : '/..';
}
}
// Lastly, append the rest of the destination (`to`) path that comes after
// the common path parts.
return `${out}${to.slice(toStart + lastCommonSep)}`;
},
toNamespacedPath(path) {
// Non-op on posix systems
return path;
},
dirname(path) {
if (path.length === 0)
return '.';
const hasRoot = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
let end = -1;
let matchedSlash = true;
for (let i = path.length - 1; i >= 1; --i) {
if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
if (!matchedSlash) {
end = i;
break;
}
} else {
// We saw the first non-path separator
matchedSlash = false;
}
}
if (end === -1)
return hasRoot ? '/' : '.';
if (hasRoot && end === 1)
return '//';
return path.slice(0, end);
},
basename(path, ext) {
let start = 0;
let end = -1;
let matchedSlash = true;
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
if (ext === path)
return '';
let extIdx = ext.length - 1;
let firstNonSlashEnd = -1;
for (let i = path.length - 1; i >= 0; --i) {
const code = path.charCodeAt(i);
if (code === CHAR_FORWARD_SLASH) {
// If we reached a path separator that was not part of a set of path
// separators at the end of the string, stop now
if (!matchedSlash) {
start = i + 1;
break;
}
} else {
if (firstNonSlashEnd === -1) {
// We saw the first non-path separator, remember this index in case
// we need it if the extension ends up not matching
matchedSlash = false;
firstNonSlashEnd = i + 1;
}
if (extIdx >= 0) {
// Try to match the explicit extension
if (code === ext.charCodeAt(extIdx)) {
if (--extIdx === -1) {
// We matched the extension, so mark this as the end of our path
// component
end = i;
}
} else {
// Extension does not match, so our result is the entire path
// component
extIdx = -1;
end = firstNonSlashEnd;
}
}
}
}
if (start === end)
end = firstNonSlashEnd;
else if (end === -1)
end = path.length;
return path.slice(start, end);
}
for (let i = path.length - 1; i >= 0; --i) {
if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
// If we reached a path separator that was not part of a set of path
// separators at the end of the string, stop now
if (!matchedSlash) {
start = i + 1;
break;
}
} else if (end === -1) {
// We saw the first non-path separator, mark this as the end of our
// path component
matchedSlash = false;
end = i + 1;
}
}
if (end === -1)
return '';
return path.slice(start, end);
},
extname(path) {
let startDot = -1;
let startPart = 0;
let end = -1;
let matchedSlash = true;
// Track the state of characters (if any) we see before our first dot and
// after any path separator we find
let preDotState = 0;
for (let i = path.length - 1; i >= 0; --i) {
const code = path.charCodeAt(i);
if (code === CHAR_FORWARD_SLASH) {
// If we reached a path separator that was not part of a set of path
// separators at the end of the string, stop now
if (!matchedSlash) {
startPart = i + 1;
break;
}
continue;
}
if (end === -1) {
// We saw the first non-path separator, mark this as the end of our
// extension
matchedSlash = false;
end = i + 1;
}
if (code === CHAR_DOT) {
// If this is our first dot, mark it as the start of our extension
if (startDot === -1)
startDot = i;
else if (preDotState !== 1)
preDotState = 1;
} else if (startDot !== -1) {
// We saw a non-dot and non-path separator before our dot, so we should
// have a good chance at having a non-empty extension
preDotState = -1;
}
}
if (startDot === -1 ||
end === -1 ||
// We saw a non-dot character immediately before the dot
preDotState === 0 ||
// The (right-most) trimmed path component is exactly '..'
(preDotState === 1 &&
startDot === end - 1 &&
startDot === startPart + 1)) {
return '';
}
return path.slice(startDot, end);
},
format: _format.bind( null, '/'),
parse(path) {
const ret = { root: '', dir: '', base: '', ext: '', name: '' };
if (path.length === 0)
return ret;
const isAbsolute =
path.charCodeAt(0) === CHAR_FORWARD_SLASH;
let start;
if (isAbsolute) {
ret.root = '/';
start = 1;
} else {
start = 0;
}
let startDot = -1;
let startPart = 0;
let end = -1;
let matchedSlash = true;
let i = path.length - 1;
// Track the state of characters (if any) we see before our first dot and
// after any path separator we find
let preDotState = 0;
// Get non-dir info
for (; i >= start; --i) {
const code = path.charCodeAt(i);
if (code === CHAR_FORWARD_SLASH) {
// If we reached a path separator that was not part of a set of path
// separators at the end of the string, stop now
if (!matchedSlash) {
startPart = i + 1;
break;
}
continue;
}
if (end === -1) {
// We saw the first non-path separator, mark this as the end of our
// extension
matchedSlash = false;
end = i + 1;
}
if (code === CHAR_DOT) {
// If this is our first dot, mark it as the start of our extension
if (startDot === -1)
startDot = i;
else if (preDotState !== 1)
preDotState = 1;
} else if (startDot !== -1) {
// We saw a non-dot and non-path separator before our dot, so we should
// have a good chance at having a non-empty extension
preDotState = -1;
}
}
if (end !== -1) {
const start = startPart === 0 && isAbsolute ? 1 : startPart;
if (startDot === -1 ||
// We saw a non-dot character immediately before the dot
preDotState === 0 ||
// The (right-most) trimmed path component is exactly '..'
(preDotState === 1 &&
startDot === end - 1 &&
startDot === startPart + 1)) {
ret.base = ret.name = path.slice(start, end);
} else {
ret.name = path.slice(start, startDot);
ret.base = path.slice(start, end);
ret.ext = path.slice(startDot, end);
}
}
if (startPart > 0)
ret.dir = path.slice(0, startPart - 1);
else if (isAbsolute)
ret.dir = '/';
return ret;
},
sep: '/',
delimiter: ':',
win32: null,
posix: null
};
function _format(sep, pathObject) {
validateObject(pathObject, 'pathObject');
const dir = pathObject.dir || pathObject.root;
const base = pathObject.base ||
`${pathObject.name || ''}${pathObject.ext || ''}`;
if (!dir) {
return base;
}
return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`;
}
export default path

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,436 @@
/**
* Parses a given response text into a JSON object. If the parsing fails due to invalid JSON format,
* the original response text is returned.
*
* @param {string} responseText - The response text to be parsed into JSON. It is expected to be a valid JSON string.
* @returns {Object|string} The parsed JSON object if the responseText is valid JSON, otherwise returns the original responseText.
* @example
* // returns { key: "value" }
* parseResponse('{"key": "value"}');
*
* @example
* // returns "Invalid JSON"
* parseResponse('Invalid JSON');
*/
async function parseResponse(target) {
if ( target.responseType === 'blob' ) {
// Get content type of the blob
const contentType = target.getResponseHeader('content-type');
if ( contentType.startsWith('application/json') ) {
// If the blob is JSON, parse it
const text = await target.response.text();
try {
return JSON.parse(text);
} catch (error) {
return text;
}
}else if ( contentType.startsWith('application/octet-stream') ) {
// If the blob is an octet stream, return the blob
return target.response;
}
// Otherwise return an ojbect
return {
success: true,
result: target.response,
};
}
const responseText = target.responseText;
try {
return JSON.parse(responseText);
} catch (error) {
return responseText;
}
}
/**
* A function that generates a UUID (Universally Unique Identifier) using the version 4 format,
* which are random UUIDs. It uses the cryptographic number generator available in modern browsers.
*
* The generated UUID is a 36 character string (32 alphanumeric characters separated by 4 hyphens).
* It follows the pattern: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, where x is any hexadecimal digit
* and y is one of 8, 9, A, or B.
*
* @returns {string} Returns a new UUID v4 string.
*
* @example
*
* let id = this.#uuidv4(); // Generate a new UUID
*
*/
function uuidv4(){
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
/**
* Initializes and returns an XMLHttpRequest object configured for a specific API endpoint, method, and headers.
*
* @param {string} endpoint - The API endpoint to which the request will be sent. This is appended to the API origin URL.
* @param {string} APIOrigin - The origin URL of the API. This is prepended to the endpoint.
* @param {string} authToken - The authorization token used for accessing the API. This is included in the request headers.
* @param {string} [method='post'] - The HTTP method to be used for the request. Defaults to 'post' if not specified.
* @param {string} [contentType='application/json;charset=UTF-8'] - The content type of the request. Defaults to
* 'application/json;charset=UTF-8' if not specified.
*
* @returns {XMLHttpRequest} The initialized XMLHttpRequest object.
*/
function initXhr(endpoint, APIOrigin, authToken, method= "post", contentType = "application/json;charset=UTF-8", responseType = undefined) {
const xhr = new XMLHttpRequest();
xhr.open(method, APIOrigin + endpoint, true);
xhr.setRequestHeader("Authorization", "Bearer " + authToken);
xhr.setRequestHeader("Content-Type", contentType);
xhr.responseType = responseType;
return xhr;
}
/**
* Handles an HTTP response by invoking appropriate callback functions and resolving or rejecting a promise.
*
* @param {Function} success_cb - An optional callback function for successful responses. It should take a response object
* as its only argument.
* @param {Function} error_cb - An optional callback function for error handling. It should take an error object
* as its only argument.
* @param {Function} resolve_func - A function used to resolve a promise. It should take a response object
* as its only argument.
* @param {Function} reject_func - A function used to reject a promise. It should take an error object
* as its only argument.
* @param {Object} response - The HTTP response object from the request. Expected to have 'status' and 'responseText'
* properties.
*
* @returns {void} The function does not return a value but will either resolve or reject a promise based on the
* response status.
*/
async function handle_resp(success_cb, error_cb, resolve_func, reject_func, response){
const resp = await parseResponse(response);
// error - unauthorized
if(response.status === 401){
// if error callback is provided, call it
if(error_cb && typeof error_cb === 'function')
error_cb({status: 401, message: 'Unauthorized'})
// reject promise
return reject_func({status: 401, message: 'Unauthorized'})
}
// error - other
else if(response.status !== 200){
// if error callback is provided, call it
if(error_cb && typeof error_cb === 'function')
error_cb(resp)
// reject promise
return reject_func(resp)
}
// success
else{
// This is a driver error
if(resp.success === false && resp.error?.code === 'permission_denied'){
let perm = await puter.ui.requestPermission({permission: 'driver:puter-image-generation:generate'});
// try sending again if permission was granted
if(perm.granted){
// todo repeat request
}
}
// if success callback is provided, call it
if(success_cb && typeof success_cb === 'function')
success_cb(resp);
// resolve with success
return resolve_func(resp);
}
}
/**
* Handles an error by invoking a specified error callback and then rejecting a promise.
*
* @param {Function} error_cb - An optional callback function that is called if it's provided.
* This function should take an error object as its only argument.
* @param {Function} reject_func - A function used to reject a promise. It should take an error object
* as its only argument.
* @param {Object} error - The error object that is passed to both the error callback and the reject function.
*
* @returns {void} The function does not return a value but will call the reject function with the error.
*/
function handle_error(error_cb, reject_func, error){
// if error callback is provided, call it
if(error_cb && typeof error_cb === 'function')
error_cb(error)
// reject promise
return reject_func(error)
}
function setupXhrEventHandlers(xhr, success_cb, error_cb, resolve_func, reject_func) {
// load: success or error
xhr.addEventListener('load', function(e){
return handle_resp(success_cb, error_cb, resolve_func, reject_func, this, xhr);
});
// error
xhr.addEventListener('error', function(e){
return handle_error(error_cb, reject_func, this);
})
}
const NOOP = () => {};
class Valid {
static callback (cb) {
return (cb && typeof cb === 'function') ? cb : undefined;
}
}
/**
* Makes the hybrid promise/callback function for a particular driver method
* @param {string[]} arg_defs - argument names (for now; definitions eventually)
* @param {string} driverInterface - name of the interface
* @param {string} driverMethod - name of the method
*/
function make_driver_method(arg_defs, driverInterface, driverMethod, settings = {}) {
return async function (...args) {
let driverArgs = {};
let options = {};
// Check if the first argument is an object and use it as named parameters
if (args.length === 1 && typeof args[0] === 'object' && !Array.isArray(args[0])) {
driverArgs = { ...args[0] };
options = {
success: driverArgs.success,
error: driverArgs.error,
};
// Remove callback functions from driverArgs if they exist
delete driverArgs.success;
delete driverArgs.error;
} else {
// Handle as individual arguments
arg_defs.forEach((argName, index) => {
driverArgs[argName] = args[index];
});
options = {
success: args[arg_defs.length],
error: args[arg_defs.length + 1],
};
}
// preprocess
if(settings.preprocess && typeof settings.preprocess === 'function'){
driverArgs = settings.preprocess(driverArgs);
}
return await driverCall(options, driverInterface, driverMethod, driverArgs, settings);
};
}
async function driverCall (options, driverInterface, driverMethod, driverArgs, settings) {
const tp = new TeePromise();
driverCall_(
options,
tp.resolve.bind(tp),
tp.reject.bind(tp),
driverInterface,
driverMethod,
driverArgs,
undefined,
undefined,
settings,
);
return await tp;
}
// This function encapsulates the logic for sending a driver call request
async function driverCall_(
options = {},
resolve_func, reject_func,
driverInterface, driverMethod, driverArgs,
method,
contentType = 'application/json;charset=UTF-8',
settings = {},
) {
// If there is no authToken and the environment is web, try authenticating with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
return reject_func({
error: {
code: 'auth_canceled', message: 'Authentication canceled'
}
})
}
}
const success_cb = Valid.callback(options.success) ?? NOOP;
const error_cb = Valid.callback(options.error) ?? NOOP;
// create xhr object
const xhr = initXhr('/drivers/call', puter.APIOrigin, puter.authToken, 'POST', contentType);
if ( settings.responseType ) {
xhr.responseType = settings.responseType;
}
// load: success or error
xhr.addEventListener('load', async function(response){
const resp = await parseResponse(response.target);
// HTTP Error - unauthorized
if(response.status === 401 || resp?.code === "token_auth_failed"){
if(resp?.code === "token_auth_failed" && puter.env === 'web'){
try{
puter.resetAuthToken();
await puter.ui.authenticateWithPuter();
}catch(e){
return reject_func({
error: {
code: 'auth_canceled', message: 'Authentication canceled'
}
})
}
}
// if error callback is provided, call it
if(error_cb && typeof error_cb === 'function')
error_cb({status: 401, message: 'Unauthorized'})
// reject promise
return reject_func({status: 401, message: 'Unauthorized'})
}
// HTTP Error - other
else if(response.status && response.status !== 200){
// if error callback is provided, call it
error_cb(resp)
// reject promise
return reject_func(resp)
}
// HTTP Success
else{
// Driver Error: permission denied
if(resp.success === false && resp.error?.code === 'permission_denied'){
let perm = await puter.ui.requestPermission({permission: 'driver:' + driverInterface + ':' + driverMethod});
// try sending again if permission was granted
if(perm.granted){
// repeat request with permission granted
return driverCall_(options, resolve_func, reject_func, driverInterface, driverMethod, driverArgs, method, contentType, settings);
}else{
// if error callback is provided, call it
error_cb(resp)
// reject promise
return reject_func(resp)
}
}
// Driver Error: other
else if(resp.success === false){
// if error callback is provided, call it
error_cb(resp)
// reject promise
return reject_func(resp)
}
let result = resp.result !== undefined ? resp.result : resp;
if ( settings.transform ) {
result = await settings.transform(result);
}
// Success: if callback is provided, call it
if(resolve_func.success)
success_cb(result);
// Success: resolve with the result
return resolve_func(result);
}
});
// error
xhr.addEventListener('error', function(e){
return handle_error(error_cb, reject_func, this);
})
// send request
xhr.send(JSON.stringify({
interface: driverInterface,
test_mode: settings?.test_mode,
method: driverMethod,
args: driverArgs,
}));
}
class TeePromise {
static STATUS_PENDING = {};
static STATUS_RUNNING = {};
static STATUS_DONE = {};
constructor () {
this.status_ = this.constructor.STATUS_PENDING;
this.donePromise = new Promise((resolve, reject) => {
this.doneResolve = resolve;
this.doneReject = reject;
});
}
get status () {
return this.status_;
}
set status (status) {
this.status_ = status;
if ( status === this.constructor.STATUS_DONE ) {
this.doneResolve();
}
}
resolve (value) {
this.status_ = this.constructor.STATUS_DONE;
this.doneResolve(value);
}
awaitDone () {
return this.donePromise;
}
then (fn, rfn) {
return this.donePromise.then(fn, rfn);
}
reject (err) {
this.status_ = this.constructor.STATUS_DONE;
this.doneReject(err);
}
/**
* @deprecated use then() instead
*/
onComplete(fn) {
return this.then(fn);
}
}
async function blob_to_url (blob) {
const tp = new TeePromise();
const reader = new FileReader();
reader.onloadend = () => tp.resolve(reader.result);
reader.readAsDataURL(blob);
return await tp;
}
function blobToDataUri(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(event) {
resolve(event.target.result);
};
reader.onerror = function(error) {
reject(error);
};
reader.readAsDataURL(blob);
});
}
function arrayBufferToDataUri(arrayBuffer) {
return new Promise((resolve, reject) => {
const blob = new Blob([arrayBuffer]);
const reader = new FileReader();
reader.onload = function(event) {
resolve(event.target.result);
};
reader.onerror = function(error) {
reject(error);
};
reader.readAsDataURL(blob);
});
}
export {parseResponse, uuidv4, handle_resp, handle_error, initXhr, setupXhrEventHandlers, driverCall,
TeePromise,
make_driver_method,
blob_to_url,
arrayBufferToDataUri,
blobToDataUri,
};

View File

@ -0,0 +1,253 @@
import * as utils from '../lib/utils.js'
class AI{
/**
* Creates a new instance with the given authentication token, API origin, and app ID,
*
* @class
* @param {string} authToken - Token used to authenticate the user.
* @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
* @param {string} appID - ID of the app to use.
*/
constructor (authToken, APIOrigin, appID) {
this.authToken = authToken;
this.APIOrigin = APIOrigin;
this.appID = appID;
}
/**
* Sets a new authentication token and resets the socket connection with the updated token, if applicable.
*
* @param {string} authToken - The new authentication token.
* @memberof [AI]
* @returns {void}
*/
setAuthToken (authToken) {
this.authToken = authToken;
}
/**
* Sets the API origin.
*
* @param {string} APIOrigin - The new API origin.
* @memberof [AI]
* @returns {void}
*/
setAPIOrigin (APIOrigin) {
this.APIOrigin = APIOrigin;
}
img2txt = async (...args) => {
let MAX_INPUT_SIZE = 10 * 1024 * 1024;
let options = {};
let testMode = false;
// Check that the argument is not undefined or null
if(!args){
throw({message: 'Arguments are required', code: 'arguments_required'});
}
// if argument is string transform it to the object that the API expects
if (typeof args[0] === 'string' || args[0] instanceof Blob) {
options.source = args[0];
}
// if input is a blob, transform it to a data URI
if (args[0].source instanceof Blob) {
options.source = await utils.blobToDataUri(args[0].source);
}
// check input size
if (options.source.length > this.MAX_INPUT_SIZE) {
throw { message: 'Input size cannot be larger than ' + MAX_INPUT_SIZE, code: 'input_too_large' };
}
// determine if test mode is enabled
if (typeof args[1] === 'boolean' && args[1] === true ||
typeof args[2] === 'boolean' && args[2] === true ||
typeof args[3] === 'boolean' && args[3] === true) {
testMode = true;
}
console.log(args, options);
return await utils.make_driver_method(['source'], 'puter-ocr', 'recognize', {
test_mode: testMode ?? false,
transform: async (result) => {
let str = '';
for (let i = 0; i < result?.blocks?.length; i++) {
if("text/textract:LINE" === result.blocks[i].type)
str += result.blocks[i].text + "\n";
}
return str;
}
}).call(this, options);
}
txt2speech = async (...args) => {
let MAX_INPUT_SIZE = 3000;
let options = {};
let testMode = false;
if(!args){
throw({message: 'Arguments are required', code: 'arguments_required'});
}
// if argument is string transform it to the object that the API expects
if (typeof args[0] === 'string') {
options = { text: args[0] };
}
// if second argument is string, it's the language
if (args[1] && typeof args[1] === 'string') {
options.language = args[1];
}
// check input size
if (options.text.length > this.MAX_INPUT_SIZE) {
throw { message: 'Input size cannot be larger than ' + MAX_INPUT_SIZE, code: 'input_too_large' };
}
// determine if test mode is enabled
if (typeof args[1] === 'boolean' && args[1] === true ||
typeof args[2] === 'boolean' && args[2] === true ||
typeof args[3] === 'boolean' && args[3] === true) {
testMode = true;
}
return await utils.make_driver_method(['source'], 'puter-tts', 'synthesize', {
responseType: 'blob',
test_mode: testMode ?? false,
transform: async (result) => {
const url = await utils.blob_to_url(result);
const audio = new Audio(url);
audio.toString = () => url;
audio.valueOf = () => url;
return audio;
}
}).call(this, options);
}
// accepts either a string or an array of message objects
// if string, it's treated as the prompt which is a shorthand for { messages: [{ content: prompt }] }
// if object, it's treated as the full argument object that the API expects
chat = async (...args) => {
let options = {};
let testMode = false;
// Check that the argument is not undefined or null
if(!args){
throw({message: 'Arguments are required', code: 'arguments_required'});
}
// ai.chat(prompt)
// ai.chat(prompt, testMode)
if (typeof args[0] === 'string' && (!args[1] || typeof args[1] === 'boolean')) {
options = { messages: [{ content: args[0] }] };
}
// ai.chat(prompt, imageURL/File)
// ai.chat(prompt, imageURL/File, testMode)
else if (typeof args[0] === 'string' && (typeof args[1] === 'string' || args[1] instanceof File)) {
// if imageURL is a File, transform it to a data URI
if(args[1] instanceof File){
args[1] = await utils.blobToDataUri(args[1]);
}
// parse args[1] as an image_url object
options = {
vision: true,
messages: [
{
content: [
args[0],
{
image_url: {
url: args[1]
}
}
],
}
]
};
}
// chat(prompt, [imageURLs])
else if (typeof args[0] === 'string' && Array.isArray(args[1])) {
// parse args[1] as an array of image_url objects
for (let i = 0; i < args[1].length; i++) {
args[1][i] = { image_url: { url: args[1][i] } };
}
options = {
vision: true,
messages: [
{
content: [
args[0],
...args[1]
],
}
]
};
}
// chat([messages])
else if (Array.isArray(args[0])) {
options = { messages: args[0] };
}
// determine if testMode is enabled
if (typeof args[1] === 'boolean' && args[1] === true ||
typeof args[2] === 'boolean' && args[2] === true ||
typeof args[3] === 'boolean' && args[3] === true) {
testMode = true;
}
// Call the original chat.complete method
return await utils.make_driver_method(['messages'], 'puter-chat-completion', 'complete', {
test_mode: testMode ?? false,
transform: async (result) => {
result.toString = () => {
return result.message?.content;
};
result.valueOf = () => {
return result.message?.content;
}
return result;
}
}).call(this, options);
}
txt2img = async (...args) => {
let options = {};
let testMode = false;
if(!args){
throw({message: 'Arguments are required', code: 'arguments_required'});
}
// if argument is string transform it to the object that the API expects
if (typeof args[0] === 'string') {
options = { prompt: args[0] };
}
// if second argument is string, it's the `testMode`
if (typeof args[1] === 'boolean' && args[1] === true) {
testMode = true;
}
// Call the original chat.complete method
return await utils.make_driver_method(['prompt'], 'puter-image-generation', 'generate', {
responseType: 'blob',
test_mode: testMode ?? false,
transform: async blob => {
let img = new Image();
img.src = await utils.blob_to_url(blob);
img.toString = () => img.src;
img.valueOf = () => img.src;
return img;
}
}).call(this, options);
}
}
export default AI;

View File

@ -0,0 +1,158 @@
import * as utils from '../lib/utils.js'
class Apps{
/**
* Creates a new instance with the given authentication token, API origin, and app ID,
*
* @class
* @param {string} authToken - Token used to authenticate the user.
* @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
* @param {string} appID - ID of the app to use.
*/
constructor (authToken, APIOrigin, appID) {
this.authToken = authToken;
this.APIOrigin = APIOrigin;
this.appID = appID;
}
/**
* Sets a new authentication token.
*
* @param {string} authToken - The new authentication token.
* @memberof [Apps]
* @returns {void}
*/
setAuthToken (authToken) {
this.authToken = authToken;
}
/**
* Sets the API origin.
*
* @param {string} APIOrigin - The new API origin.
* @memberof [Apps]
* @returns {void}
*/
setAPIOrigin (APIOrigin) {
this.APIOrigin = APIOrigin;
}
list = async (...args) => {
let options = {};
options = { "predicate": ['user-can-edit'] };
return utils.make_driver_method(['uid'], 'puter-apps', 'select').call(this, options);
}
create = async (...args) => {
let options = {};
// * allows for: puter.apps.new('example-app') *
if (typeof args[0] === 'string') {
let indexURL = args[1];
let title = args[2] ?? args[0];
options = { object: { name: args[0], index_url: indexURL, title: title}};
}else if (typeof args[0] === 'object' && args[0] !== null) {
let options_raw = args[0];
options = {
object: {
name: options_raw.name,
index_url: options_raw.indexURL,
title: options_raw.title,
description: options_raw.description,
icon: options_raw.icon,
maximize_on_start: options_raw.maximizeOnStart,
filetype_associations: options_raw.filetypeAssociations,
}
};
}
// Call the original chat.complete method
return await utils.make_driver_method(['object'], 'puter-apps', 'create').call(this, options);
}
update = async(...args) => {
let options = {};
// if there is one string argument, assume it is the app name
// * allows for: puter.apps.update('example-app') *
if (Array.isArray(args) && typeof args[0] === 'string') {
let object_raw = args[1];
let object = {
name: object_raw.name,
index_url: object_raw.indexURL,
title: object_raw.title,
description: object_raw.description,
icon: object_raw.icon,
maximize_on_start: object_raw.maximizeOnStart,
filetype_associations: object_raw.filetypeAssociations,
};
options = { id: { name: args[0]}, object: object};
}
// Call the original chat.complete method
return await utils.make_driver_method(['object'], 'puter-apps', 'update').call(this, options);
}
get = async(...args) => {
let options = {};
// if there is one string argument, assume it is the app name
// * allows for: puter.apps.get('example-app') *
if (Array.isArray(args) && typeof args[0] === 'string') {
options = { id: {name: args[0]}};
}
return utils.make_driver_method(['uid'], 'puter-apps', 'read').call(this, options);
}
delete = async(...args) => {
let options = {};
// if there is one string argument, assume it is the app name
// * allows for: puter.apps.get('example-app') *
if (Array.isArray(args) && typeof args[0] === 'string') {
options = { id: {name: args[0]}};
}
return utils.make_driver_method(['uid'], 'puter-apps', 'delete').call(this, options);
}
getDeveloperProfile = function(...args){
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
success: args[0],
error: args[1],
};
}
return new Promise((resolve, reject) => {
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
success: args[0],
error: args[1],
};
}
return new Promise((resolve, reject) => {
const xhr = utils.initXhr('/get-dev-profile', puter.APIOrigin, puter.authToken, 'get');
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
xhr.send();
})
});
}
}
export default Apps;

View File

@ -0,0 +1,115 @@
import * as utils from '../lib/utils.js'
class Auth{
// Used to generate a unique message id for each message sent to the host environment
// we start from 1 because 0 is falsy and we want to avoid that for the message id
#messageID = 1;
/**
* Creates a new instance with the given authentication token, API origin, and app ID,
*
* @class
* @param {string} authToken - Token used to authenticate the user.
* @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
* @param {string} appID - ID of the app to use.
*/
constructor (authToken, APIOrigin, appID) {
this.authToken = authToken;
this.APIOrigin = APIOrigin;
this.appID = appID;
}
/**
* Sets a new authentication token.
*
* @param {string} authToken - The new authentication token.
* @memberof [Auth]
* @returns {void}
*/
setAuthToken (authToken) {
this.authToken = authToken;
}
/**
* Sets the API origin.
*
* @param {string} APIOrigin - The new API origin.
* @memberof [Auth]
* @returns {void}
*/
setAPIOrigin (APIOrigin) {
this.APIOrigin = APIOrigin;
}
signIn = () =>{
return new Promise((resolve, reject) => {
let msg_id = this.#messageID++;
let w = 600;
let h = 600;
let title = 'Puter';
var left = (screen.width/2)-(w/2);
var top = (screen.height/2)-(h/2);
window.open(puter.defaultGUIOrigin + '/action/sign-in?embedded_in_popup=true&msg_id=' + msg_id,
title,
'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
window.addEventListener('message', function(e){
if(e.data.msg_id == msg_id){
// remove redundant attributes
delete e.data.msg_id;
delete e.data.msg;
if(e.data.success){
// set the auth token
puter.setAuthToken(e.data.token);
resolve(e.data);
}else
reject(e.data);
// delete the listener
window.removeEventListener('message', this);
}
});
});
}
isSignedIn = () =>{
if(puter.authToken)
return true;
else
return false;
}
getUser = function(...args){
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
success: args[0],
error: args[1],
};
}
return new Promise((resolve, reject) => {
const xhr = utils.initXhr('/whoami', puter.APIOrigin, puter.authToken, 'get');
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
xhr.send();
})
}
signOut = () =>{
puter.resetAuthToken();
}
}
export default Auth

View File

@ -0,0 +1,98 @@
import path from "../lib/path.js"
class FSItem{
constructor(options){
this.readURL = options.readURL ?? options.read_url;
this.writeURL = options.writeURL ?? options.write_url;
this.metadataURL = options.metadataURL ?? options.metadata_url;
this.name = options.name ?? options.fsentry_name;
this.uid = options.uid ?? options.uuid ?? options.fsentry_uid ?? options.fsentry_id ?? options.fsentry_uuid ?? options.id;
this.id = this.uid;
this.uuid = this.uid;
this.path = options.path ?? options.fsentry_path;
this.size = options.size ?? options.fsentry_size;
this.accessed = options.accessed ?? options.fsentry_accessed;
this.modified = options.modified ?? options.fsentry_modified;
this.created = options.created ?? options.fsentry_created;
this.isDirectory = (options.isDirectory || options.is_dir || options.fsentry_is_dir) ? true : false;
}
write = async function(data){
return puter.fs.write(
this.path,
new File([data], this.name),
{
overwrite: true,
dedupeName: false,
},
);
}
// Watches for changes to the item, and calls the callback function
// with the new data when a change is detected.
watch = function(callback){
// todo - implement
}
open = function(callback){
// todo - implement
}
// Set wallpaper
setAsWallpaper = function(options, callback){
// todo - implement
}
rename = function(new_name){
return puter.fs.rename(this.uid, new_name);
}
move = function(dest_path, overwrite=false, new_name){
return puter.fs.move(this.path, dest_path, overwrite, new_name);
}
copy = function(destination_directory, auto_rename=false, overwrite=false){
return puter.fs.copy(this.path, destination_directory, auto_rename, overwrite);
}
delete = function(){
return puter.fs.delete(this.path);
}
versions = async function(){
// todo - implement
}
trash = function(){
// todo make sure puter allows for moving to trash by default
// todo implement trashing
}
mkdir = async function(name, auto_rename = false){
// Don't proceed if this is not a directory, throw error
if(!this.isDirectory)
throw new Error('mkdir() can only be called on a directory');
// mkdir
return puter.fs.mkdir(path.join(this.path, name));
}
metadata = async function(){
// todo - implement
}
readdir = async function(){
// Don't proceed if this is not a directory, throw error
if(!this.isDirectory)
throw new Error('readdir() can only be called on a directory');
// readdir
return puter.fs.readdir(this.path);
}
read = async function(){
return puter.fs.read(this.path);
}
}
export default FSItem;

View File

@ -0,0 +1,133 @@
import io from '../../lib/socket.io/socket.io.esm.min.js';
// Operations
import readdir from "./operations/readdir.js";
import stat from "./operations/stat.js";
import space from "./operations/space.js";
import mkdir from "./operations/mkdir.js";
import copy from "./operations/copy.js";
import rename from "./operations/rename.js";
import upload from "./operations/upload.js";
import read from "./operations/read.js";
import move from "./operations/move.js";
import write from "./operations/write.js";
import sign from "./operations/sign.js";
// Why is this called deleteFSEntry instead of just delete? because delete is
// a reserved keyword in javascript
import deleteFSEntry from "./operations/deleteFSEntry.js";
class FileSystem{
readdir = readdir;
stat = stat;
space = space;
mkdir = mkdir;
copy = copy;
rename = rename;
upload = upload;
read = read;
// Why is this called deleteFSEntry instead of just delete? because delete is
// a reserved keyword in javascript.
delete = deleteFSEntry;
move = move;
write = write;
sign = sign;
/**
* Creates a new instance with the given authentication token, API origin, and app ID,
* and connects to the socket.
*
* @class
* @param {string} authToken - Token used to authenticate the user.
* @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
* @param {string} appID - ID of the app to use.
*/
constructor (authToken, APIOrigin, appID) {
this.authToken = authToken;
this.APIOrigin = APIOrigin;
this.appID = appID;
// Connect socket.
this.initializeSocket();
}
/**
* Initializes the socket connection to the server using the current API origin.
* If a socket connection already exists, it disconnects it before creating a new one.
* Sets up various event listeners on the socket to handle different socket events like
* connect, disconnect, reconnect, reconnect_attempt, reconnect_error, reconnect_failed, and error.
*
* @memberof FileSystem
* @returns {void}
*/
initializeSocket() {
if (this.socket) {
this.socket.disconnect();
}
this.socket = io(this.APIOrigin, {
query: {
auth_token: this.authToken,
}
});
this.bindSocketEvents();
}
bindSocketEvents() {
this.socket.on('connect', () => {
console.log('FileSystem Socket: Connected', this.socket.id);
});
this.socket.on('disconnect', () => {
console.log('FileSystem Socket: Disconnected');
});
this.socket.on('reconnect', (attempt) => {
console.log('FileSystem Socket: Reconnected', this.socket.id);
});
this.socket.on('reconnect_attempt', (attempt) => {
console.log('FileSystem Socket: Reconnection Attemps', attempt);
});
this.socket.on('reconnect_error', (error) => {
console.log('FileSystem Socket: Reconnection Error', error);
});
this.socket.on('reconnect_failed', () => {
console.log('FileSystem Socket: Reconnection Failed');
});
this.socket.on('error', (error) => {
console.error('FileSystem Socket Error:', error);
});
}
/**
* Sets a new authentication token and resets the socket connection with the updated token.
*
* @param {string} authToken - The new authentication token.
* @memberof [FileSystem]
* @returns {void}
*/
setAuthToken (authToken) {
this.authToken = authToken;
// reset socket
this.initializeSocket();
}
/**
* Sets the API origin and resets the socket connection with the updated API origin.
*
* @param {string} APIOrigin - The new API origin.
* @memberof [Apps]
* @returns {void}
*/
setAPIOrigin (APIOrigin) {
this.APIOrigin = APIOrigin;
// reset socket
this.initializeSocket();
}
}
export default FileSystem;

View File

@ -0,0 +1,61 @@
import * as utils from '../../../lib/utils.js';
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
const copy = function (...args) {
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
source: args[0],
destination: args[1],
overwrite: args[2]?.overwrite,
new_name: args[2]?.newName || args[2]?.new_name,
create_missing_parents: args[2]?.createMissingParents || args[2]?.create_missing_parents,
new_metadata: args[2]?.newMetadata || args[2]?.new_metadata,
original_client_socket_id: args[2]?.excludeSocketID || args[2]?.original_client_socket_id,
success: args[3],
error: args[4],
// Add more if needed...
};
}
return new Promise(async (resolve, reject) => {
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject('Authentication failed.');
}
}
// convert paths to absolute path
options.source = getAbsolutePathForApp(options.source);
options.destination = getAbsolutePathForApp(options.destination);
// create xhr object
const xhr = utils.initXhr('/copy', this.APIOrigin, this.authToken);
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
xhr.send(JSON.stringify({
original_client_socket_id: this.socket.id,
socket_id: this.socket.id,
source: options.source,
destination: options.destination,
overwrite: options.overwrite,
new_name: (options.new_name || options.newName),
// if user is copying an item to where its source is, change the name so there is no conflict
dedupe_name: (options.dedupe_name || options.dedupeName),
}));
})
}
export default copy;

View File

@ -0,0 +1,59 @@
import * as utils from '../../../lib/utils.js';
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
// why is this called deleteFSEntry instead of just delete?
// because delete is a reserved keyword in javascript
const deleteFSEntry = async function(...args) {
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
}
// Otherwise, we assume separate arguments are provided
else {
options = {
paths: args[0],
recursive: args[1]?.recursive ?? true,
descendantsOnly: args[1]?.descendantsOnly ?? false,
};
}
// If paths is a string, convert to array
// this is to make it easier for the user to provide a single path without having to wrap it in an array
let paths = options.paths;
if(typeof paths === 'string')
paths = [paths];
return new Promise(async (resolve, reject) => {
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject('Authentication failed.');
}
}
// create xhr object
const xhr = utils.initXhr('/delete', this.APIOrigin, this.authToken);
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
// convert paths to absolute paths
paths = paths.map((path) => {
return getAbsolutePathForApp(path);
})
xhr.send(JSON.stringify({
paths: paths,
descendants_only: (options.descendants_only || options.descendantsOnly) ?? false,
recursive: options.recursive ?? true,
}));
})
}
export default deleteFSEntry;

View File

@ -0,0 +1,59 @@
import * as utils from '../../../lib/utils.js';
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
import path from "../../../lib/path.js"
const mkdir = function (...args) {
let options = {};
// If first argument is a string and the second is an object, or if the first is an object
if ((typeof args[0] === 'string' && typeof args[1] === 'object' && !(args[1] instanceof Function)) || (typeof args[0] === 'object' && args[0] !== null)) {
// If it's a string followed by an object, it means path then options
if (typeof args[0] === 'string') {
options.path = args[0];
// Merge the options
Object.assign(options, args[1]);
options.success = args[2];
options.error = args[3];
} else {
options = args[0];
}
} else if (typeof args[0] === 'string') {
// it means it's a path then functions (success and optionally error)
options.path = args[0];
options.success = args[1];
options.error = args[2];
}
return new Promise(async (resolve, reject) => {
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject('Authentication failed.');
}
}
// create xhr object
const xhr = utils.initXhr('/mkdir', this.APIOrigin, this.authToken);
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
options.path = getAbsolutePathForApp(options.path);
xhr.send(JSON.stringify({
parent: path.dirname(options.path),
path: path.basename(options.path),
overwrite: options.overwrite ?? false,
dedupe_name: (options.rename || options.dedupeName) ?? false,
shortcut_to: options.shortcutTo,
original_client_socket_id: this.socket.id,
create_missing_parents: (options.recursive || options.createMissingParents) ?? false,
}));
})
}
export default mkdir;

View File

@ -0,0 +1,57 @@
import * as utils from '../../../lib/utils.js';
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
const move = function (...args) {
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
source: args[0],
destination: args[1],
overwrite: args[2]?.overwrite,
new_name: args[2]?.newName || args[2]?.new_name,
create_missing_parents: args[2]?.createMissingParents || args[2]?.create_missing_parents,
new_metadata: args[2]?.newMetadata || args[2]?.new_metadata,
original_client_socket_id: args[2]?.excludeSocketID || args[2]?.original_client_socket_id,
};
}
return new Promise(async (resolve, reject) => {
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject('Authentication failed.');
}
}
// convert source and destination to absolute path
options.source = getAbsolutePathForApp(options.source);
options.destination = getAbsolutePathForApp(options.destination);
// create xhr object
const xhr = utils.initXhr('/move', this.APIOrigin, this.authToken);
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
xhr.send(JSON.stringify({
source: options.source,
destination: options.destination,
overwrite: options.overwrite,
new_name: (options.new_name || options.newName),
create_missing_parents: (options.create_missing_parents || options.createMissingParents),
new_metadata: (options.new_metadata || options.newMetadata),
original_client_socket_id: options.excludeSocketID,
}));
})
}
export default move;

View File

@ -0,0 +1,44 @@
import * as utils from '../../../lib/utils.js';
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
const read = function (...args) {
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
path: typeof args[0] === 'string' ? args[0] : (typeof args[0] === 'object' && args[0] !== null ? args[0].path : args[0]),
success: args[1],
error: args[2],
};
}
return new Promise(async (resolve, reject) => {
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject('Authentication failed.');
}
}
// convert path to absolute path
options.path = getAbsolutePathForApp(options.path);
// create xhr object
const xhr = utils.initXhr('/read?file=' + encodeURIComponent(options.path), this.APIOrigin, this.authToken, 'get', "application/json;charset=UTF-8", 'blob');
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
xhr.send();
})
}
export default read;

View File

@ -0,0 +1,46 @@
import * as utils from '../../../lib/utils.js';
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
const readdir = async function (...args) {
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
path: args[0],
success: args[1],
error: args[2],
};
}
return new Promise(async (resolve, reject) => {
// path is required
if(!options.path){
throw new Error({ code: 'NO_PATH', message: 'No path provided.' });
}
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject('Authentication failed.');
}
}
// create xhr object
const xhr = utils.initXhr('/readdir', this.APIOrigin, this.authToken);
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
xhr.send(JSON.stringify({path: getAbsolutePathForApp(options.path)}));
})
}
export default readdir;

View File

@ -0,0 +1,56 @@
import * as utils from '../../../lib/utils.js';
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
const rename = function (...args) {
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
path: args[0],
new_name: args[1],
success: args[2],
error: args[3],
// Add more if needed...
};
}
return new Promise(async (resolve, reject) => {
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject('Authentication failed.');
}
}
// create xhr object
const xhr = utils.initXhr('/rename', this.APIOrigin, this.authToken);
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
let dataToSend = {
original_client_socket_id: options.excludeSocketID || options.original_client_socket_id,
new_name: options.new_name || options.newName,
};
if (options.uid !== undefined) {
dataToSend.uid = options.uid;
} else if (options.path !== undefined) {
// If dirPath is not provided or it's not starting with a slash, it means it's a relative path
// in that case, we need to prepend the app's root directory to it
dataToSend.path = getAbsolutePathForApp(options.path);
}
xhr.send(JSON.stringify(dataToSend));
})
}
export default rename;

View File

@ -0,0 +1,103 @@
import * as utils from '../../../lib/utils.js';
/**
* Signs a file system entry or entries and optionally calls a provided callback with the result.
* If a single item is passed, it is converted into an array.
* Sends a POST request to the server to sign the items.
*
* @param {(Object|Object[])} items - The file system entry or entries to be signed. Can be a single object or an array of objects.
* @param {function} [callback] - Optional callback function to be invoked with the result of the signing.
* @returns {(Object|Object[])} If a single item was passed, returns a single object. If multiple items were passed, returns an array of objects.
* @throws {Error} If the AJAX request fails.
* @async
*/
const sign = function(...args){
let options;
// Otherwise, we assume separate arguments are provided
options = {
app_uid: args[0],
items: args[1],
success: args[2],
error: args[3],
// Add more if needed...
};
return new Promise(async (resolve, reject) => {
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject('Authentication failed.');
}
}
let items = options.items;
// if only a single item is passed, convert it to array
// so that the code below can work with arrays
if(!Array.isArray(items)){
items = [items]
}
// create xhr object
const xhr = utils.initXhr('/sign', this.APIOrigin, this.authToken);
// response
xhr.addEventListener('load', async function(e){
const resp = await utils.parseResponse(this);
// error
if(this.status !== 200){
// if error callback is provided, call it
if(options.error && typeof options.error === 'function')
options.error(resp)
// reject promise
return reject(resp)
}
// success
else{
let res = resp;
let result;
let token = res.token;
// if only a single item was passed, return a single object
if(items.length == 1){
result = {...(res.signatures[0])};
}
// if multiple items were passed, return an array of objects
else{
let obj=[];
for(let i=0; i<res.signatures.length; i++){
obj.push({...res.signatures[i]});
}
result = obj;
}
// if success callback is provided, call it
if(options.success && typeof options.success === 'function')
options.success({token: token, items: result});
// resolve with success
return resolve({token: token, items: result});
}
});
xhr.upload.addEventListener('progress', function(e){
})
// error
xhr.addEventListener('error', function(e){
return utils.handle_error(options.error, reject, this);
})
xhr.send(JSON.stringify({
app_uid: options.app_uid,
items: items
}));
})
}
export default sign;

View File

@ -0,0 +1,40 @@
import * as utils from '../../../lib/utils.js';
const space = function (...args) {
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
success: args[0],
error: args[1],
// Add more if needed...
};
}
return new Promise(async (resolve, reject) => {
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject('Authentication failed.');
}
}
// create xhr object
const xhr = utils.initXhr('/df', this.APIOrigin, this.authToken);
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
xhr.send();
})
}
export default space;

View File

@ -0,0 +1,57 @@
import * as utils from '../../../lib/utils.js';
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
const stat = async function (...args) {
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
path: args[0],
options: typeof args[1] === 'object' ? args[1] : {},
success: typeof args[1] === 'object' ? args[2] : args[1],
error: typeof args[1] === 'object' ? args[3] : args[2],
// Add more if needed...
};
}
return new Promise(async (resolve, reject) => {
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject('Authentication failed.');
}
}
// create xhr object
const xhr = utils.initXhr('/stat', this.APIOrigin, this.authToken);
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
let dataToSend = {};
if (options.uid !== undefined) {
dataToSend.uid = options.uid;
} else if (options.path !== undefined) {
// If dirPath is not provided or it's not starting with a slash, it means it's a relative path
// in that case, we need to prepend the app's root directory to it
dataToSend.path = getAbsolutePathForApp(options.path);
}
dataToSend.return_subdomains = options.returnSubdomains;
dataToSend.return_permissions = options.returnPermissions;
dataToSend.return_versions = options.returnVersions;
dataToSend.return_size = options.returnSize;
xhr.send(JSON.stringify(dataToSend));
})
}
export default stat;

View File

@ -0,0 +1,419 @@
import * as utils from '../../../lib/utils.js';
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
import path from "../../../lib/path.js"
const upload = async function(items, dirPath, options = {}){
return new Promise(async (resolve, reject) => {
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject(e);
}
}
// xhr object to be used for the upload
let xhr = new XMLHttpRequest();
// Can not write to root
if(dirPath === '/'){
// if error callback is provided, call it
if(options.error && typeof options.error === 'function')
options.error('Can not upload to root directory.');
return reject('Can not upload to root directory.');
}
// If dirPath is not provided or it's not starting with a slash, it means it's a relative path
// in that case, we need to prepend the app's root directory to it
dirPath = getAbsolutePathForApp(dirPath);
// Generate a unique ID for this upload operation
// This will be used to uniquely identify this operation and its progress
// across servers and clients
const operation_id = utils.uuidv4();
// Call 'init' callback if provided
// init is basically a hook that allows the user to get the operation ID and the XMLHttpRequest object
if(options.init && typeof options.init === 'function'){
options.init(operation_id, xhr);
}
// keeps track of the amount of data uploaded to the server
let bytes_uploaded_to_server = 0;
// keeps track of the amount of data uploaded to the cloud
let bytes_uploaded_to_cloud = 0;
// This will hold the normalized entries to be uploaded
// Since 'items' could be a DataTransferItemList, FileList, File, or an array of any of these,
// we need to normalize it into an array of consistently formatted objects which will be held in 'entries'
let entries;
// will hold the total size of the upload
let total_size = 0;
let file_count = 0;
let seemsToBeParsedDataTransferItems = false;
if(Array.isArray(items) && items.length > 0){
for(let i=0; i<items.length; i++){
if(items[i] instanceof DataTransferItem || items[i] instanceof DataTransferItemList){
seemsToBeParsedDataTransferItems = true;
}else{
}
}
}
// DataTransferItemList
if(items instanceof DataTransferItemList || items instanceof DataTransferItem || items[0] instanceof DataTransferItem || options.parsedDataTransferItems){
// if parsedDataTransferItems is true, it means the user has already parsed the DataTransferItems
if(options.parsedDataTransferItems)
entries = items;
else
entries = await puter.ui.getEntriesFromDataTransferItems(items);
// Sort entries by size ascending
entries.sort((entry_a, entry_b) => {
if ( entry_a.isDirectory && ! entry_b.isDirectory ) return -1;
if ( ! entry_a.isDirectory && entry_b.isDirectory ) return 1;
if ( entry_a.isDirectory && entry_b.isDirectory ) return 0;
return entry_a.size - entry_b.size;
});
}
// FileList/File
else if(items instanceof File || items[0] instanceof File || items instanceof FileList || items[0] instanceof FileList){
if(!Array.isArray(items))
entries = items instanceof FileList ? Array.from(items) : [items];
else
entries = items;
// Sort entries by size ascending
entries.sort((entry_a, entry_b) => {
return entry_a.size - entry_b.size;
})
// add FullPath property to each entry
for(let i=0; i<entries.length; i++){
entries[i].filepath = entries[i].name;
entries[i].fullPath = entries[i].name;
}
}
// blob
else if(items instanceof Blob){
// create a File object from the blob
let file = new File([items], options.name, { type: "text/plain" });
entries = [file];
// add FullPath property to each entry
for(let i=0; i<entries.length; i++){
entries[i].filepath = entries[i].name;
entries[i].fullPath = entries[i].name;
}
}
// String
else if(typeof items === 'string'){
// create a File object from the string
let file = new File([items], 'default.txt', { type: "text/plain" });
entries = [file];
// add FullPath property to each entry
for(let i=0; i<entries.length; i++){
entries[i].filepath = entries[i].name;
entries[i].fullPath = entries[i].name;
}
}
// Will hold directories and files to be uploaded
let dirs = [];
let files = [];
// Separate files from directories
for(let i=0; i<entries.length; i++){
// skip empty entries
if(!entries[i])
continue;
//collect dirs
if(entries[i].isDirectory)
dirs.push({path: path.join(dirPath, entries[i].finalPath ? entries[i].finalPath : entries[i].fullPath)});
// also files
else
files.push(entries[i])
// stats about the upload to come
if(entries[i].size !== undefined){
total_size += (entries[i].size);
file_count++;
}
}
// Continue only if there are actually any files/directories to upload
if(dirs.length === 0 && files.length === 0){
if(options.error && typeof options.error === 'function'){
options.error({code: 'EMPTY_UPLOAD', message: 'No files or directories to upload.'});
}
return reject({code: 'EMPTY_UPLOAD', message: 'No files or directories to upload.'});
}
// Check storage capacity.
// We need to check the storage capacity before the upload starts because
// we want to avoid uploading files in case there is not enough storage space.
// If we didn't check before upload starts, we could end up in a scenario where
// the user uploads a very large folder/file and then the server rejects it because there is not enough space
//
// Space check in 'web' environment is currently not supported since it requires permissions.
let storage;
if(puter.env !== 'web'){
try{
storage = await this.space();
if(storage.capacity - storage.used < total_size){
if(options.error && typeof options.error === 'function'){
options.error({code: 'NOT_ENOUGH_SPACE', message: 'Not enough storage space available.'});
}
return reject({code: 'NOT_ENOUGH_SPACE', message: 'Not enough storage space available.'});
}
}catch(e){
}
}
// total size of the upload is doubled because we will be uploading the files to the server
// and then the server will upload them to the cloud
total_size = total_size * 2;
// holds the data to be sent to the server
const fd = new FormData();
//-------------------------------------------------
// Generate the requests to create all the
// folders in this upload
//-------------------------------------------------
dirs.sort();
let mkdir_requests = [];
for(let i=0; i < dirs.length; i++){
// update all file paths under this folder if dirname was changed
for(let j=0; j<files.length; j++){
// if file is in this folder and has not been processed yet
if(!files[j].puter_path_param && path.join(dirPath, files[j].filepath).startsWith((dirs[i].path) + '/')){
files[j].puter_path_param = `$dir_${i}/`+ path.basename(files[j].filepath);
}
}
// update all subdirs under this dir
for(let k=0; k < dirs.length; k++){
if(!dirs[k].puter_path_param && dirs[k].path.startsWith(dirs[i].path + '/')){
dirs[k].puter_path_param = `$dir_${i}/`+ path.basename(dirs[k].path);
}
}
}
for(let i=0; i < dirs.length; i++){
let parent_path = path.dirname(dirs[i].puter_path_param || dirs[i].path);
let dir_path = dirs[i].puter_path_param || dirs[i].path;
// remove parent path from the beginning of path since path is relative to parent
if(parent_path !== '/')
dir_path = dir_path.replace(parent_path, '');
mkdir_requests.push({
op: 'mkdir',
parent: parent_path,
path: dir_path,
overwrite: options.overwrite ?? false,
dedupe_name: options.dedupeName ?? true,
create_missing_ancestors: options.createMissingAncestors ?? true,
as: `dir_${i}`,
});
}
// inverse mkdir_requests so that the root folder is created first
// and then go down the tree
mkdir_requests.reverse();
fd.append('operation_id', operation_id);
fd.append('socket_id', this.socket.id);
fd.append('original_client_socket_id', this.socket.id);
// Append mkdir operations to upload request
for(let i=0; i<mkdir_requests.length; i++){
fd.append('operation', JSON.stringify(mkdir_requests[i]));
}
// Append file metadata to upload request
if(!options.shortcutTo){
for(let i=0; i<files.length; i++){
fd.append('fileinfo', JSON.stringify({
name: files[i].name,
type: files[i].type,
size: files[i].size,
}));
}
}
// Append write operations for each file
for(let i=0; i<files.length; i++){
fd.append('operation', JSON.stringify({
op: options.shortcutTo ? 'shortcut' : 'write',
dedupe_name: options.dedupeName ?? true,
overwrite: options.overwrite ?? false,
create_missing_ancestors: (options.createMissingAncestors || options.createMissingParents),
operation_id: operation_id,
path: (
files[i].puter_path_param &&
path.dirname(files[i].puter_path_param ?? '')
) || (
files[i].filepath &&
path.join(dirPath, path.dirname(files[i].filepath))
) || '',
name: path.basename(files[i].filepath),
item_upload_id: i,
shortcut_to: options.shortcutTo,
shortcut_to_uid: options.shortcutTo,
app_uid: options.appUID,
}));
}
// Append files to upload
if(!options.shortcutTo){
for(let i=0; i<files.length; i++){
fd.append('file', files[i] ?? '');
}
}
const progress_handler = (msg) => {
if(msg.operation_id === operation_id){
bytes_uploaded_to_cloud += msg.loaded_diff
}
}
// Handle upload progress events from server
this.socket.on('upload.progress', progress_handler);
// keeps track of the amount of data uploaded to the server
let previous_chunk_uploaded = null;
// open request to server
xhr.open("post",(this.APIOrigin +'/batch'), true);
// set auth header
xhr.setRequestHeader("Authorization", "Bearer " + this.authToken);
// -----------------------------------------------
// Upload progress: client -> server
// -----------------------------------------------
xhr.upload.addEventListener('progress', function(e){
// update operation tracker
let chunk_uploaded;
if(previous_chunk_uploaded === null){
chunk_uploaded = e.loaded;
previous_chunk_uploaded = 0;
}else{
chunk_uploaded = e.loaded - previous_chunk_uploaded;
}
previous_chunk_uploaded += chunk_uploaded;
bytes_uploaded_to_server += chunk_uploaded;
// overall operation progress
let op_progress = ((bytes_uploaded_to_cloud + bytes_uploaded_to_server)/total_size * 100).toFixed(2);
op_progress = op_progress > 100 ? 100 : op_progress;
// progress callback function
if(options.progress && typeof options.progress === 'function')
options.progress(operation_id, op_progress);
})
// -----------------------------------------------
// Upload progress: server -> cloud
// the following code will check the progress of the upload every 100ms
// -----------------------------------------------
let cloud_progress_check_interval = setInterval(function() {
// operation progress
let op_progress = ((bytes_uploaded_to_cloud + bytes_uploaded_to_server)/total_size * 100).toFixed(2);
op_progress = op_progress > 100 ? 100 : op_progress;
if(options.progress && typeof options.progress === 'function')
options.progress(operation_id, op_progress);
}, 100);
// -----------------------------------------------
// onabort
// -----------------------------------------------
xhr.onabort = ()=>{
// stop the cloud upload progress tracker
clearInterval(cloud_progress_check_interval);
// remove progress handler
this.socket.off('upload.progress', progress_handler);
// if an 'abort' callback is provided, call it
if(options.abort && typeof options.abort === 'function')
options.abort(operation_id);
}
// -----------------------------------------------
// on success/error
// -----------------------------------------------
xhr.onreadystatechange = async (e)=>{
if (xhr.readyState === 4) {
const resp = await utils.parseResponse(xhr);
// Error
if((xhr.status >= 400 && xhr.status < 600) || (options.strict && xhr.status === 218)) {
// stop the cloud upload progress tracker
clearInterval(cloud_progress_check_interval);
// remove progress handler
this.socket.off('upload.progress', progress_handler);
// If this is a 'strict' upload (i.e. status code is 218), we need to find out which operation failed
// and call the error callback with that operation.
if(options.strict && xhr.status === 218){
// find the operation that failed
let failed_operation;
for(let i=0; i<resp.results?.length; i++){
if(resp.results[i].status !== 200){
failed_operation = resp.results[i];
break;
}
}
// if error callback is provided, call it
if(options.error && typeof options.error === 'function'){
options.error(failed_operation);
}
return reject(failed_operation);
}
// if error callback is provided, call it
if(options.error && typeof options.error === 'function'){
options.error(resp);
}
return reject(resp);
}
// Success
else{
if(!resp || !resp.results || resp.results.length === 0){
// no results
console.log('no results');
}
let items = resp.results;
items = items.length === 1 ? items[0] : items;
// if success callback is provided, call it
if(options.success && typeof options.success === 'function'){
options.success(items);
}
// stop the cloud upload progress tracker
clearInterval(cloud_progress_check_interval);
// remove progress handler
this.socket.off('upload.progress', progress_handler);
return resolve(items);
}
}
}
// Fire off the 'start' event
if(options.start && typeof options.start === 'function'){
options.start();
}
// send request
xhr.send(fd);
})
}
export default upload;

View File

@ -0,0 +1,53 @@
import path from "../../../lib/path.js"
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
const write = async function (targetPath, data, options = {}) {
// targetPath is required
if(!targetPath){
throw new Error({ code: 'NO_TARGET_PATH', message: 'No target path provided.' });
}
// if targetPath is a File
if(targetPath instanceof File && data === undefined){
data = targetPath;
targetPath = data.name;
}
// strict mode will cause the upload to fail if even one operation fails
// for example, if one of the files in a folder fails to upload, the entire upload will fail
// since write is a wrapper around upload to handle single-file uploads, we need to pass the strict option to upload
options.strict = true;
// by default, we want to overwrite existing files
options.overwrite = options.overwrite ?? true;
// if overwrite is true and dedupeName is not provided, set dedupeName to false
if(options.overwrite && options.dedupeName === undefined)
options.dedupeName = false;
// if targetPath is not provided or it's not starting with a slash, it means it's a relative path
// in that case, we need to prepend the app's root directory to it
targetPath = getAbsolutePathForApp(targetPath);
// extract file name from targetPath
const filename = path.basename(targetPath);
// extract the parent directory from targetPath
const parent = path.dirname(targetPath);
// if data is a string, convert it to a File object
if(typeof data === 'string'){
data = new File([data ?? ''], filename ?? 'Untitled.txt', { type: "text/plain" });
}
// blob
else if(data instanceof Blob){
data = new File([data ?? ''], filename ?? 'Untitled', { type: data.type });
}
if(!data)
data = new File([data ?? ''], filename);
// perform upload
return this.upload(data, parent, options);
}
export default write;

View File

@ -0,0 +1,21 @@
import path from "../../../lib/path.js"
const getAbsolutePathForApp = (relativePath)=>{
// if we are in the gui environment, return the relative path as is
if(puter.env === 'gui')
return relativePath;
// if no relative path is provided, use the current working directory
if(!relativePath)
relativePath = '.';
// If relativePath is not provided, or it's not starting with a slash or tilde,
// it means it's a relative path. In that case, prepend the app's root directory.
if (!relativePath || (!relativePath.startsWith('/') && !relativePath.startsWith('~') && puter.appID)) {
relativePath = path.join('~/AppData', puter.appID, relativePath);
}
return relativePath;
}
export default getAbsolutePathForApp;

View File

@ -0,0 +1,136 @@
import * as utils from '../lib/utils.js';
import getAbsolutePathForApp from './FileSystem/utils/getAbsolutePathForApp.js';
class Hosting{
/**
* Creates a new instance with the given authentication token, API origin, and app ID,
*
* @class
* @param {string} authToken - Token used to authenticate the user.
* @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
* @param {string} appID - ID of the app to use.
*/
constructor (authToken, APIOrigin, appID) {
this.authToken = authToken;
this.APIOrigin = APIOrigin;
this.appID = appID;
}
/**
* Sets a new authentication token.
*
* @param {string} authToken - The new authentication token.
* @memberof [Router]
* @returns {void}
*/
setAuthToken (authToken) {
this.authToken = authToken;
}
/**
* Sets the API origin.
*
* @param {string} APIOrigin - The new API origin.
* @memberof [Apps]
* @returns {void}
*/
setAPIOrigin (APIOrigin) {
this.APIOrigin = APIOrigin;
}
// todo document the `Subdomain` object.
list = utils.make_driver_method([], 'puter-subdomains', 'select');
create = async (...args) => {
let options = {};
// * allows for: puter.hosting.new('example-subdomain') *
if (typeof args[0] === 'string' && args.length === 1) {
// if subdomain is in the format of a `subdomain.puter.site` or `subdomain.puter.com`, extract the subdomain
// and use it as the subdomain. This is to make development easier.
if (args[0].match(/^[a-z0-9]+\.puter\.(site|com)$/)) {
args[0] = args[0].split('.')[0];
}
options = { object: { subdomain: args[0] }};
}
// if there are two string arguments, assume they are the subdomain and the target directory
// * allows for: puter.hosting.new('example-subdomain', '/path/to/target') *
else if (Array.isArray(args) && args.length === 2 && typeof args[0] === 'string') {
// if subdomain is in the format of a `subdomain.puter.site` or `subdomain.puter.com`, extract the subdomain
// and use it as the subdomain. This is to make development easier.
if (args[0].match(/^[a-z0-9]+\.puter\.(site|com)$/)) {
args[0] = args[0].split('.')[0];
}
// if the target directory is not an absolute path, make it an absolute path relative to the app's root directory
if(args[1]){
args[1] = getAbsolutePathForApp(args[1]);
}
options = { object: { subdomain: args[0], root_dir: args[1] }};
}
// allows for: puter.hosting.new({ subdomain: 'subdomain' })
else if (typeof args[0] === 'object') {
options = { object: args[0] };
}
// Call the original chat.complete method
return await utils.make_driver_method(['object'], 'puter-subdomains', 'create').call(this, options);
}
update = async(...args) => {
let options = {};
// If there are two string arguments, assume they are the subdomain and the target directory
// * allows for: puter.hosting.update('example-subdomain', '/path/to/target') *
if (Array.isArray(args) && typeof args[0] === 'string') {
// if subdomain is in the format of a `subdomain.puter.site` or `subdomain.puter.com`, extract the subdomain
// and use it as the subdomain. This is to make development easier.
if (args[0].match(/^[a-z0-9]+\.puter\.(site|com)$/)) {
args[0] = args[0].split('.')[0];
}
// if the target directory is not an absolute path, make it an absolute path relative to the app's root directory
if(args[1]){
args[1] = getAbsolutePathForApp(args[1]);
}
options = { id: {subdomain: args[0]}, object: { root_dir: args[1] ?? null }};
}
// Call the original chat.complete method
return await utils.make_driver_method(['object'], 'puter-subdomains', 'update').call(this, options);
}
get = async(...args) => {
let options = {};
// if there is one string argument, assume it is the subdomain
// * allows for: puter.hosting.get('example-subdomain') *
if (Array.isArray(args) && typeof args[0] === 'string') {
// if subdomain is in the format of a `subdomain.puter.site` or `subdomain.puter.com`, extract the subdomain
// and use it as the subdomain. This is to make development easier.
if (args[0].match(/^[a-z0-9]+\.puter\.(site|com)$/)) {
args[0] = args[0].split('.')[0];
}
options = { id: {subdomain: args[0]}};
}
return utils.make_driver_method(['uid'], 'puter-subdomains', 'read').call(this, options);
}
delete = async(...args) => {
let options = {};
// if there is one string argument, assume it is the subdomain
// * allows for: puter.hosting.get('example-subdomain') *
if (Array.isArray(args) && typeof args[0] === 'string') {
// if subdomain is in the format of a `subdomain.puter.site` or `subdomain.puter.com`, extract the subdomain
// and use it as the subdomain. This is to make development easier.
if (args[0].match(/^[a-z0-9]+\.puter\.(site|com)$/)) {
args[0] = args[0].split('.')[0];
}
options = { id: {subdomain: args[0]}};
}
return utils.make_driver_method(['uid'], 'puter-subdomains', 'delete').call(this, options);
}
}
export default Hosting;

View File

@ -0,0 +1,205 @@
import * as utils from '../lib/utils.js'
class KV{
MAX_KEY_SIZE = 1024;
MAX_VALUE_SIZE = 400 * 1024;
/**
* Creates a new instance with the given authentication token, API origin, and app ID,
*
* @class
* @param {string} authToken - Token used to authenticate the user.
* @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
* @param {string} appID - ID of the app to use.
*/
constructor (authToken, APIOrigin, appID) {
this.authToken = authToken;
this.APIOrigin = APIOrigin;
this.appID = appID;
}
/**
* Sets a new authentication token.
*
* @param {string} authToken - The new authentication token.
* @memberof [KV]
* @returns {void}
*/
setAuthToken (authToken) {
this.authToken = authToken;
}
/**
* Sets the API origin.
*
* @param {string} APIOrigin - The new API origin.
* @memberof [KV]
* @returns {void}
*/
setAPIOrigin (APIOrigin) {
this.APIOrigin = APIOrigin;
}
/**
* Resolves to 'true' on success, or rejects with an error on failure
*
* `key` cannot be undefined or null.
* `key` size cannot be larger than 1mb.
* `value` size cannot be larger than 10mb.
*/
set = utils.make_driver_method(['key', 'value'], 'puter-kvstore', 'set',{
preprocess: (args)=>{
// key cannot be undefined or null
if(args.key === undefined || args.key === null){
throw { message: 'Key cannot be undefined', code: 'key_undefined'};
}
// key size cannot be larger than MAX_KEY_SIZE
if(args.key.length > this.MAX_KEY_SIZE){
throw {message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'};
}
// value size cannot be larger than MAX_VALUE_SIZE
if(args.value && args.value.length > this.MAX_VALUE_SIZE){
throw {message: 'Value size cannot be larger than ' + this.MAX_VALUE_SIZE, code: 'value_too_large'};
}
return args;
}
})
/**
* Resolves to the value if the key exists, or `undefined` if the key does not exist. Rejects with an error on failure.
*/
get = utils.make_driver_method(['key'], 'puter-kvstore', 'get', {
preprocess: (args)=>{
// key size cannot be larger than MAX_KEY_SIZE
if(args.key.length > this.MAX_KEY_SIZE){
throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
}
return args;
},
transform: (res)=>{
return res;
}
})
incr = async(...args) => {
let options = {};
// arguments are required
if(!args || args.length === 0){
throw ({message: 'Arguments are required', code: 'arguments_required'});
}
options.key = args[0];
options.amount = args[1] ?? 1;
// key size cannot be larger than MAX_KEY_SIZE
if(options.key.length > this.MAX_KEY_SIZE){
throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
}
return utils.make_driver_method(['key'], 'puter-kvstore', 'incr').call(this, options);
}
decr = async(...args) => {
let options = {};
// arguments are required
if(!args || args.length === 0){
throw ({message: 'Arguments are required', code: 'arguments_required'});
}
options.key = args[0];
options.amount = args[1] ?? 1;
// key size cannot be larger than MAX_KEY_SIZE
if(options.key.length > this.MAX_KEY_SIZE){
throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
}
return utils.make_driver_method(['key'], 'puter-kvstore', 'decr').call(this, options);
}
// resolves to 'true' on success, or rejects with an error on failure
// will still resolve to 'true' if the key does not exist
del = utils.make_driver_method(['key'], 'puter-kvstore', 'del', {
preprocess: (args)=>{
// key size cannot be larger than this.MAX_KEY_SIZE
if(args.key.length > this.MAX_KEY_SIZE){
throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
}
return args;
}
});
list = async(...args) => {
let options = {};
let pattern;
let returnValues = false;
// list(true) or list(pattern, true) will return the key-value pairs
if((args && args.length === 1 && args[0] === true) || (args && args.length === 2 && args[1] === true)){
options = {};
returnValues = true;
}
// return only the keys, default behavior
else{
options = { as: 'keys'};
}
// list(pattern)
// list(pattern, true)
if((args && args.length === 1 && typeof args[0] === 'string') || (args && args.length === 2 && typeof args[0] === 'string' && args[1] === true)){
pattern = args[0];
}
return utils.make_driver_method([], 'puter-kvstore', 'list', {
transform: (res)=>{
// glob pattern was provided
if(pattern){
// consider both the key and the value
if(!returnValues) {
let keys = res.filter((key)=>{
return globMatch(pattern, key);
});
return keys;
}else{
let keys = res.filter((key_value_pair)=>{
return globMatch(pattern, key_value_pair.key);
});
return keys;
}
}
return res;
}
}).call(this, options);
}
// resolve to 'true' on success, or rejects with an error on failure
// will still resolve to 'true' if there are no keys
flush = utils.make_driver_method([], 'puter-kvstore', 'flush')
// clear is an alias for flush
clear = this.flush;
}
function globMatch(pattern, str) {
const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
let regexPattern = escapeRegExp(pattern)
.replace(/\\\*/g, '.*') // Replace * with .*
.replace(/\\\?/g, '.') // Replace ? with .
.replace(/\\\[/g, '[') // Replace [ with [
.replace(/\\\]/g, ']') // Replace ] with ]
.replace(/\\\^/g, '^'); // Replace ^ with ^
let re = new RegExp('^' + regexPattern + '$');
return re.test(str);
}
export default KV

View File

@ -0,0 +1,90 @@
import * as utils from '../lib/utils.js'
class OS{
/**
* Creates a new instance with the given authentication token, API origin, and app ID,
*
* @class
* @param {string} authToken - Token used to authenticate the user.
* @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
* @param {string} appID - ID of the app to use.
*/
constructor (authToken, APIOrigin, appID) {
this.authToken = authToken;
this.APIOrigin = APIOrigin;
this.appID = appID;
}
/**
* Sets a new authentication token.
*
* @param {string} authToken - The new authentication token.
* @memberof [OS]
* @returns {void}
*/
setAuthToken (authToken) {
this.authToken = authToken;
}
/**
* Sets the API origin.
*
* @param {string} APIOrigin - The new API origin.
* @memberof [Apps]
* @returns {void}
*/
setAPIOrigin (APIOrigin) {
this.APIOrigin = APIOrigin;
}
user = function(...args){
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
success: args[0],
error: args[1],
};
}
return new Promise((resolve, reject) => {
const xhr = utils.initXhr('/whoami', this.APIOrigin, this.authToken, 'get');
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
xhr.send();
})
}
version = function(...args){
let options;
// If first argument is an object, it's the options
if (typeof args[0] === 'object' && args[0] !== null) {
options = args[0];
} else {
// Otherwise, we assume separate arguments are provided
options = {
success: args[0],
error: args[1],
// Add more if needed...
};
}
return new Promise((resolve, reject) => {
const xhr = utils.initXhr('/version', this.APIOrigin, this.authToken, 'get');
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
xhr.send();
})
}
}
export default OS

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,619 @@
naughtyStrings = [
"文件.txt", // Chinese characters
"файл.txt", // Cyrillic characters
"ファイル.txt", // Japanese characters
"파일.txt", // Korean characters
"ملف.txt", // Arabic characters
"फ़ाइल.txt", // Hindi characters
"archivo.txt", // Spanish characters
"fichier.txt", // French characters
"αρχείο.txt", // Greek characters
"datei.txt", // German characters
"fil.txt", // Swedish characters
"קובץ.txt", // Hebrew characters
"文件名.txt", // Chinese characters
"файлы.txt", // Russian characters
"फ़ाइलें.txt", // Hindi characters
"📄_emoji.txt", // Emoji
"file name with spaces.txt",
"file-name-with-dashes.txt",
"file_name_with_underscores.txt",
"file.name.with.periods.txt",
"file,name,with,commas.txt",
"file;name;with;semicolons.txt",
"file(name)with(parentheses).txt",
"file[name]with[brackets].txt",
"file{name}with{braces}.txt",
"file!name!with!exclamations!.txt",
"file@name@with@ats.txt",
"file#name#with#hashes#.txt",
"file$name$with$dollars$.txt",
"file%name%with%percentages%.txt",
"file^name^with^carats^.txt",
"file&name&with&amps&.txt",
"file*name*with*asterisks*.txt",
"file_name_with_long_name_exceeding_255_characters_abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.txt",
"file👍name👍with👍thumbs👍up.txt",
"file😂name😂with😂emojis😂.txt",
"file🌍name🌍with🌍globe🌍emojis🌍.txt",
"file🔥name🔥with🔥fire🔥emoji🔥.txt",
"file🎉name🎉with🎉party🎉popper🎉emoji🎉.txt",
"file💼name💼with💼briefcase💼emoji💼.txt",
"file🍔name🍔with🍔burger🍔emoji🍔.txt",
"file🚀name🚀with🚀rocket🚀emoji🚀.txt",
"file👽name👽with👽alien👽emoji👽.txt",
"file🌈name🌈with🌈rainbow🌈emoji🌈.txt",
"file🍆name🍆with🍆eggplant🍆emoji🍆.txt",
"file🍑name🍑with🍑peach🍑emoji🍑.txt",
"invisible\u200Bname.txt", // Invisible Unicode character (Zero Width Space)
"invisible\u200Cname.txt", // Invisible Unicode character (Zero Width Non-Joiner)
"invisible\u200Dname.txt", // Invisible Unicode character (Zero Width Joiner)
"invisible\uFEFFname.txt", // Invisible Unicode character (Zero Width No-Break Space)
"invisible\u180Ename.txt", // Invisible Unicode character (Mongolian Vowel Separator)
"hash#tag.txt",
"percent%20encoded.txt",
"plus+sign.txt",
"ampersand&symbol.txt",
"at@symbol.txt",
"parentheses(1).txt",
"brackets[1].txt",
"curly{braces}.txt",
"angle<tags>.txt",
"exclamation!point.txt",
"question?mark.txt",
"colon:separated.txt",
"semicolon;separated.txt",
"single'quote.txt",
"double\"quote.txt",
"backtick`char.txt",
"tilde~sign.txt",
"underscore_character.txt",
"hyphen-character.txt",
"equal=sign.txt",
"plus+sign.txt",
"asterisk*char.txt",
"caret^char.txt",
"percent%sign.txt",
"dollar$sign.txt",
"pound#sign.txt",
"at@sign.txt",
"exclamation!mark.txt",
"question?mark.txt",
"backslash\\char.txt",
"pipe|char.txt",
"colon:char.txt",
"semicolon;char.txt",
"quote'char.txt",
"double\"quote.txt",
"backtick`char.txt",
"braces{char}.txt",
"brackets[char].txt",
"parentheses(char).txt",
"angle<brackets>.txt",
"ellipsis….txt",
"accentué.txt",
"ümlaut.txt",
"tildeñ.txt",
"çedilla.txt",
"špecial.txt",
"russianЯ.txt",
"chinese中文.txt",
"arabicعربى.txt",
"hebrewעברית.txt",
"japanese日本語.txt",
"korean한국어.txt",
"vietnameseTiếng Việt.txt",
]
window.fsTests = [
testFSWrite = async ()=>{
try {
let randName = puter.randName();
const result = await puter.fs.write(randName, 'testValue');
assert(result.uid, "Failed to write to file");
pass("testFSWrite passed");
// delete the file
try {
await puter.fs.delete(randName);
} catch (error) {
throw("testFSWrite failed to delete file:", error);
}
} catch (error) {
console.log(error);
throw("testFSWrite failed:", error);
}
},
testFSRead = async ()=>{
try {
let randName = puter.randName();
await puter.fs.write(randName, 'testValue');
const result = await (await puter.fs.read(randName)).text();
assert(result === 'testValue', "Failed to read from file");
pass("testFSRead passed");
// delete the file
try {
await puter.fs.delete(randName);
} catch (error) {
fail("testFSRead failed to delete file:", error);
}
} catch (error) {
fail("testFSRead failed:", error);
}
},
testFSWriteWithoutData = async ()=>{
try {
let randName = puter.randName();
const result = await puter.fs.write(randName);
assert(result.uid, "Failed to write to file");
pass("testFSWriteWithoutData passed");
if(randName !== result.name) {
fail(`testFSWriteWithoutData failed: Names do not match ${randName} ${result.name}`);
}
// delete the file
try {
await puter.fs.delete(randName);
} catch (error) {
fail("testFSWriteWithoutData failed to delete file:", error);
}
} catch (error) {
fail("testFSWriteWithoutData failed:", error);
}
},
testFSReadWithoutData = async ()=>{
try {
let randName = puter.randName();
await puter.fs.write(randName);
const result = await (await puter.fs.read(randName)).text();
assert(result === '', "Failed to read from file");
pass("testFSReadWithoutData passed");
// delete the file
try {
await puter.fs.delete(randName);
} catch (error) {
fail("testFSReadWithoutData failed to delete file:", error);
}
} catch (error) {
fail("testFSReadWithoutData failed:", error);
}
},
testFSWriteToExistingFile = async ()=>{
try {
let randName = puter.randName();
await puter.fs.write(randName, 'testValue');
const result = await puter.fs.write(randName, 'updatedValue');
assert(result.uid, "Failed to write to file");
pass("testFSWriteToExistingFile passed");
// delete the file
try {
await puter.fs.delete(randName);
} catch (error) {
fail("testFSWriteToExistingFile failed to delete file:", error);
}
} catch (error) {
fail("testFSWriteToExistingFile failed:", error);
}
},
testFSWriteToExistingFileWithoutOverwriteAndDedupe = async ()=>{
try {
let randName = puter.randName();
await puter.fs.write(randName, 'testValue');
const result = await puter.fs.write(randName, 'updatedValue', { overwrite: false, dedupeName: false });
assert(!result.uid, "Failed to write to file");
fail("testFSWriteToExistingFileWithoutOverwriteAndDedupe failed");
// delete the file
try {
await puter.fs.delete(randName);
} catch (error) {
fail("testFSWriteToExistingFileWithoutOverwriteAndDedupe failed to delete file:", error);
}
} catch (error) {
pass("testFSWriteToExistingFileWithoutOverwriteAndDedupe passed");
}
},
testFSWriteToExistingFileWithoutOverwriteButWithDedupe = async ()=>{
try {
let randName = puter.randName();
await puter.fs.write(randName, 'testValue');
const result = await puter.fs.write(randName, 'updatedValue', { overwrite: false, dedupeName: true });
assert(result.uid, "Failed to write to file");
pass("testFSWriteToExistingFileWithoutOverwriteButWithDedupe passed");
// delete the file
try {
await puter.fs.delete(randName);
} catch (error) {
fail("testFSWriteToExistingFileWithoutOverwriteButWithDedupe failed to delete file:", error);
}
} catch (error) {
fail("testFSWriteToExistingFileWithoutOverwriteButWithDedupe failed:", error);
}
},
testFSWriteToExistingFileWithOverwriteButWithoutDedupe = async ()=>{
try {
let randName = puter.randName();
await puter.fs.write(randName, 'testValue');
const result = await puter.fs.write(randName, 'updatedValue', { overwrite: true, dedupeName: false });
assert(result.uid, "Failed to write to file");
pass("testFSWriteToExistingFileWithOverwriteButWithoutDedupe passed");
// delete the file
try {
await puter.fs.delete(randName);
} catch (error) {
fail("testFSWriteToExistingFileWithOverwriteButWithoutDedupe failed to delete file:", error);
}
} catch (error) {
fail("testFSWriteToExistingFileWithOverwriteButWithoutDedupe failed:", error);
}
},
// create a directory
testFSCreateDir = async ()=>{
try {
let randName = puter.randName();
const result = await puter.fs.mkdir(randName);
assert(result.uid, "Failed to create directory");
pass("testFSCreateDir passed");
} catch (error) {
fail("testFSCreateDir failed:", error);
}
},
// write a number of files to a directory and list them
testFSReadDir = async ()=>{
try {
let randName = puter.randName();
await puter.fs.mkdir(randName);
await puter.fs.write(randName + '/file1', 'testValue');
await puter.fs.write(randName + '/file2', 'testValue');
await puter.fs.write(randName + '/file3', 'testValue');
const result = await puter.fs.readdir(randName);
assert(result.length === 3, "Failed to read directory");
pass("testFSReadDir passed");
} catch (error) {
fail("testFSReadDir failed:", error);
}
},
// create a file then delete it
testFSDelete = async ()=>{
try {
let randName = puter.randName();
await puter.fs.write(randName, 'testValue');
const result = await puter.fs.delete(randName);
assert(!result.uid, "Failed to delete file");
pass("testFSDelete passed");
} catch (error) {
fail("testFSDelete failed:", error);
}
},
// create a directory, write a number of files to it, then delete it
testFSDeleteDir = async ()=>{
try {
let randName = puter.randName();
await puter.fs.mkdir(randName);
await puter.fs.write(randName + '/file1', 'testValue');
await puter.fs.write(randName + '/file2', 'testValue');
await puter.fs.write(randName + '/file3', 'testValue');
const result = await puter.fs.delete(randName);
assert(!result.uid, "Failed to delete directory");
pass("testFSDeleteDir passed");
} catch (error) {
fail("testFSDeleteDir failed:", error);
}
},
// attempt to delete a non-existent file
testFSDeleteNonExistentFile = async ()=>{
try {
let randName = puter.randName();
const result = await puter.fs.delete(randName);
assert(!result.uid, "Failed to delete non-existent file");
pass("testFSDeleteNonExistentFile passed");
} catch (error) {
if(error.code !== "subject_does_not_exist")
fail("testFSDeleteNonExistentFile failed:", error);
else
pass("testFSDeleteNonExistentFile passed");
}
},
// attempt to access a non-existent file
testFSReadNonExistentFile = async ()=>{
try {
let randName = puter.randName();
const result = await puter.fs.read(randName);
fail("testFSReadNonExistentFile failed");
} catch (error) {
if(error.code !== "subject_does_not_exist")
fail("testFSReadNonExistentFile failed:", error);
else
pass("testFSReadNonExistentFile passed");
}
},
testFSWriteWithSpecialCharacters = async ()=>{
let randName
try {
randName = 'testFileWithSpecialCharacte rs!@#$%^&*()_+{}|:"<>?`~'
const result = await puter.fs.write(randName, 'testValue', { specialCharacters: true });
assert(result.uid, "Failed to write to file");
pass("testFSWriteWithSpecialCharacters passed");
} catch (error) {
fail("testFSWriteWithSpecialCharacters failed:", error);
}
// delete the file
try {
await puter.fs.delete(randName);
} catch (error) {
fail("testFSWriteWithSpecialCharacters failed to delete file:", error);
}
},
testFSReadWithSpecialCharacters = async ()=>{
try {
let randName = 'testFileWithSpecialCharacte rs!@#$%^&*()_+{}|:"<>?`~'
await puter.fs.write(randName, 'testValue');
const result = await (await puter.fs.read(randName)).text();
assert(result === 'testValue', "Failed to read from file");
pass("testFSReadWithSpecialCharacters passed");
} catch (error) {
fail("testFSReadWithSpecialCharacters failed:", error);
}
},
testFSWriteLargeFile = async ()=>{
try {
let randName = puter.randName();
const result = await puter.fs.write(randName, 'testValue'.repeat(100000));
assert(result.uid, "Failed to write to file");
pass("testFSWriteLargeFile passed");
} catch (error) {
fail("testFSWriteLargeFile failed:", error);
}
},
testFSReadLargeFile = async ()=>{
try {
let randName = puter.randName();
await puter.fs.write(randName, 'testValue'.repeat(100000));
const result = await (await puter.fs.read(randName)).text();
assert(result === 'testValue'.repeat(100000), "Failed to read from file");
pass("testFSReadLargeFile passed");
} catch (error) {
fail("testFSReadLargeFile failed:", error);
}
},
testFSRenameFile = async ()=>{
try {
let randName = puter.randName();
let randName2 = puter.randName();
await puter.fs.write(randName, 'testValue');
const result = await puter.fs.rename(randName, randName2);
assert(result.name, "Failed to rename file");
pass("testFSRenameFile passed");
// check that the old file is gone
try {
await puter.fs.read(randName);
fail("testFSRenameFile failed to delete old file");
} catch (error) {
if(error.code !== "subject_does_not_exist")
fail("testFSRenameFile failed to delete old file:", error);
else
pass("testFSRenameFile passed");
}
} catch (error) {
fail("testFSRenameFile failed:", error);
}
},
testFSMoveFile = async ()=>{
try {
let randName = puter.randName();
let randName2 = puter.randName();
await puter.fs.write(randName, 'testValue');
await puter.fs.mkdir(randName2);
let result = await puter.fs.move(randName, randName2);
assert(result.moved, "Failed to move file");
// check that the old file is gone
try {
await puter.fs.read(randName);
fail("testFSMoveFile failed to delete old file");
} catch (error) {
if(error.code !== "subject_does_not_exist")
fail("testFSMoveFile failed to delete old file:", error);
else
pass("testFSMoveFile passed");
}
} catch (error) {
fail("testFSMoveFile failed:", error);
}
},
testFSCopyFile = async ()=>{
try {
let randName = puter.randName();
let randName2 = puter.randName();
await puter.fs.write(randName, 'testValue');
await puter.fs.mkdir(randName2);
let result = await puter.fs.copy(randName, randName2);
assert(Array.isArray(result) && result[0].uid, "Failed to copy file");
// check that the old file is still there
try {
await puter.fs.read(randName);
pass("testFSCopyFile passed");
} catch (error) {
fail("testFSCopyFile failed to keep old file:", error);
}
} catch (error) {
fail("testFSCopyFile failed:", error);
}
},
// copy a file to a directory with newName option
testFSCopyFileWithNewName = async ()=>{
try {
let randName = puter.randName();
let randName2 = puter.randName();
await puter.fs.write(randName, 'testValue');
await puter.fs.mkdir(randName2);
let result = await puter.fs.copy(randName, randName2, { newName: 'newName' });
assert(Array.isArray(result) && result[0].uid, "Failed to copy file");
// check file name
assert(result[0].name === 'newName', "Failed to copy file with new name");
// check that the old file is still there
try {
await puter.fs.read(randName);
pass("testFSCopyFileWithNewName passed");
} catch (error) {
fail("testFSCopyFileWithNewName failed to keep old file:", error);
}
} catch (error) {
fail("testFSCopyFileWithNewName failed:", error);
}
},
testFSStat = async ()=>{
try {
let randName = puter.randName();
await puter.fs.write(randName, 'testValue');
let result = await puter.fs.stat(randName);
assert(result.uid, "Failed to stat file");
pass("testFSStat passed");
} catch (error) {
fail("testFSStat failed:", error);
}
},
testFSStatDir = async ()=>{
try {
let randName = puter.randName();
await puter.fs.mkdir(randName);
let result = await puter.fs.stat(randName);
assert(result.uid, "Failed to stat directory");
pass("testFSStatDir passed");
} catch (error) {
fail("testFSStatDir failed:", error);
}
},
testFSStatNonExistent = async ()=>{
try {
let randName = puter.randName();
let result = await puter.fs.stat(randName);
fail("testFSStatNonExistent failed");
} catch (error) {
if(error.code !== "subject_does_not_exist")
fail("testFSStatNonExistent failed:", error);
else
pass("testFSStatNonExistent passed");
}
},
// create a directory, write a number of files to it, then delete it
testFSDeleteDirWithFiles = async ()=>{
try {
let randName = puter.randName();
await puter.fs.mkdir(randName);
await puter.fs.write(randName + '/file1', 'testValue');
await puter.fs.write(randName + '/file2', 'testValue');
await puter.fs.write(randName + '/file3', 'testValue');
const result = await puter.fs.delete(randName, { recursive: true });
assert(!result.uid, "Failed to delete directory");
pass("testFSDeleteDirWithFiles passed");
} catch (error) {
fail("testFSDeleteDirWithFiles failed:", error);
}
},
// check if stat on a directory returns name, path, is_dir
testFSStatDirReturnsAttrs = async ()=>{
try {
let randName = puter.randName();
await puter.fs.mkdir(randName);
let result = await puter.fs.stat(randName);
assert(result.name && typeof result.name === 'string', "Failed to stat directory (name)");
assert(result.path && typeof result.path === 'string', "Failed to stat directory (path)");
assert(result.immutable !== undefined, "Failed to stat directory (immutable)");
assert(result.metadata !== undefined, "Failed to stat directory (metadata)");
assert(result.modified !== undefined, "Failed to stat directory (modified)");
assert(result.created !== undefined, "Failed to stat directory (created)");
assert(result.accessed !== undefined, "Failed to stat directory (accessed)");
assert(result.size !== undefined, "Failed to stat directory (size)");
assert(result.layout !== undefined, "Failed to stat directory (layout)");
assert(result.owner !== undefined && typeof result.owner === 'object', "Failed to stat directory (owner)");
assert(result.dirname !== undefined && typeof result.dirname === 'string', "Failed to stat directory (dirname)");
assert(result.parent_id !== undefined && typeof result.parent_id === 'string', "Failed to stat directory (parent_id)");
// todo this will fail for now until is_dir is turned into boolean
assert(result.is_dir !== undefined && typeof result.is_dir === 'boolean' && result.is_dir === true, "Failed to stat directory (is_dir)");
assert(result.is_empty !== undefined && typeof result.is_empty === 'boolean', "Failed to stat directory (is_empty)");
pass("testFSStatDirReturnsAttrs passed");
} catch (error) {
throw("testFSStatDirReturnsAttrs failed:", error);
}
},
// test read() with the object returned by write()
testFSReadWithWriteResult = async ()=>{
try {
let randName = puter.randName();
let writeResult = await puter.fs.write(randName, 'testValue');
let result = await (await puter.fs.read(writeResult)).text();
assert(result === 'testValue', "Failed to read from file");
pass("testFSReadWithWriteResult passed");
// delete the file
try {
await puter.fs.delete(randName);
} catch (error) {
fail("testFSReadWithWriteResult failed to delete file:", error);
}
} catch (error) {
fail("testFSReadWithWriteResult failed:", error);
}
},
// test stat() with the object returned by write()
testFSStatWithWriteResult = async ()=>{
try {
let randName = puter.randName();
let writeResult = await puter.fs.write(randName, 'testValue');
let result = await puter.fs.stat(writeResult);
assert(result.uid, "Failed to stat file");
pass("testFSStatWithWriteResult passed");
// delete the file
try {
await puter.fs.delete(randName);
} catch (error) {
fail("testFSStatWithWriteResult failed to delete file:", error);
}
} catch (error) {
fail("testFSStatWithWriteResult failed:", error);
}
},
// test creating files with names from naughtyStrings
testFSWriteWithNaughtyStrings = async ()=>{
try {
let randName = puter.randName();
for(let i = 0; i < naughtyStrings.length; i++) {
let filename = randName + naughtyStrings[i];
console.log(filename);
let result = await puter.fs.write(filename, 'testValue');
assert(result.uid, "Failed to write to file");
// check name
assert(result.name === filename, "Failed to write to file with naughty name: " + filename);
// delete the file
try {
await puter.fs.delete(filename);
} catch (error) {
fail("testFSWriteWithNaughtyStrings failed to delete file: " + filename, error);
}
}
pass("testFSWriteWithNaughtyStrings passed");
} catch (error) {
console.log(error);
fail("testFSWriteWithNaughtyStrings failed:", error);
}
},
];

View File

@ -0,0 +1,417 @@
window.kvTests = [
testSetKeyWithValue = async function() {
try {
const result = await puter.kv.set('testKey', 'testValue');
assert(result === true, "Failed to set key with value");
pass("testSetKeyWithValue passed");
} catch (error) {
fail("testSetKeyWithValue failed:", error);
}
},
testUpdateKey = async function() {
try {
await puter.kv.set('updateKey', 'initialValue');
const result = await puter.kv.set('updateKey', 'updatedValue');
assert(result === true, "Failed to update existing key");
pass("testUpdateKey passed");
} catch (error) {
fail("testUpdateKey failed:", error);
}
},
testKeySizeLimit = async function() {
try {
const largeKey = 'a'.repeat(1025); // 1 KB + 1 byte
await puter.kv.set(largeKey, 'value');
fail("testKeySizeLimit failed: No error thrown for large key");
} catch (error) {
pass("testKeySizeLimit passed:", error.message);
}
},
testInvalidParameters = async function() {
try {
await puter.kv.set(undefined, 'value');
fail("testInvalidParameters failed: No error thrown for undefined key");
} catch (error) {
pass("testInvalidParameters passed:", error.message);
}
},
// testEmptyKey should fail
testEmptyKey = async function() {
try {
await puter.kv.set('', 'value');
fail("testEmptyKey failed: No error thrown for empty key");
} catch (error) {
pass("testEmptyKey passed:", error.message);
}
},
testSetNullValue = async function() {
try {
const result = await puter.kv.set('nullValueKey', null);
assert(result === true, "Failed to set null value");
pass("testSetNullValue passed");
} catch (error) {
fail("testSetNullValue failed:", error);
}
},
testSetObjectValue = async function() {
try {
const result = await puter.kv.set('objectKey', { a: 1 });
assert(result === true, "Failed to set object as value");
pass("testSetObjectValue passed");
} catch (error) {
fail("testSetObjectValue failed:", error);
}
},
testSetKeyWithSpecialCharacters = async function() {
try {
const result = await puter.kv.set('special@Key#', 'value');
assert(result === true, "Failed to set key with special characters");
pass("testSetKeyWithSpecialCharacters passed");
} catch (error) {
fail("testSetKeyWithSpecialCharacters failed:", error);
}
},
testSetLargeValue = async function() {
try {
const largeValue = 'a'.repeat(10000); // 10 KB
const result = await puter.kv.set('largeValueKey', largeValue);
assert(result === true, "Failed to set large value");
pass("testSetLargeValue passed");
} catch (error) {
fail("testSetLargeValue failed:", error);
}
},
testSetBooleanValue = async function() {
try {
const result = await puter.kv.set('booleanKey', true);
assert(result === true, "Failed to set boolean value");
pass("testSetBooleanValue passed");
} catch (error) {
fail("testSetBooleanValue failed:", error);
}
},
testSetNumericKey = async function() {
try {
const result = await puter.kv.set(123, 'value');
assert(result === true, "Failed to set numeric key");
pass("testSetNumericKey passed");
} catch (error) {
fail("testSetNumericKey failed:", error);
}
},
testSetConcurrentKeys = async function() {
try {
const promises = [puter.kv.set('key1', 'value1'), puter.kv.set('key2', 'value2')];
const results = await Promise.all(promises);
assert(results.every(result => result === true), "Failed to set concurrent keys");
pass("testSetConcurrentKeys passed");
} catch (error) {
fail("testSetConcurrentKeys failed:", error);
}
},
testSetValueAndRetrieve = async function() {
try {
await puter.kv.set('retrieveKey', 'testValue');
const value = await puter.kv.get('retrieveKey');
assert(value === 'testValue', "Failed to retrieve correct value");
pass("testSetValueAndRetrieve passed");
} catch (error) {
fail("testSetValueAndRetrieve failed:", error);
}
},
testUpdateValueAndRetrieve = async function() {
try {
await puter.kv.set('updateKey', 'initialValue');
await puter.kv.set('updateKey', 'updatedValue');
const value = await puter.kv.get('updateKey');
assert(value === 'updatedValue', "Failed to retrieve updated value");
pass("testUpdateValueAndRetrieve passed");
} catch (error) {
fail("testUpdateValueAndRetrieve failed:", error);
}
},
testSetNumericValueAndRetrieve = async function() {
try {
await puter.kv.set('numericKey', 123);
const value = await puter.kv.get('numericKey');
assert(value === 123, "Failed to retrieve numeric value");
pass("testSetNumericValueAndRetrieve passed");
} catch (error) {
fail("testSetNumericValueAndRetrieve failed:", error);
}
},
testSetBooleanValueAndRetrieve = async function() {
try {
await puter.kv.set('booleanKey', true);
const value = await puter.kv.get('booleanKey');
assert(value === true, "Failed to retrieve boolean value");
pass("testSetBooleanValueAndRetrieve passed");
} catch (error) {
fail("testSetBooleanValueAndRetrieve failed:", error);
}
},
testSetAndDeleteKey = async function() {
try {
await puter.kv.set('deleteKey', 'value');
const result = await puter.kv.del('deleteKey');
assert(result === true, "Failed to delete key");
pass("testSetAndDeleteKey passed");
} catch (error) {
fail("testSetAndDeleteKey failed:", error);
}
},
// if key does not exist, get() should return null
testGetNonexistentKey = async function() {
try {
const value = await puter.kv.get('nonexistentKey_102mk');
assert(value === null, "Failed to return `null` for nonexistent key");
pass("testGetNonexistentKey passed");
} catch (error) {
fail("testGetNonexistentKey failed:", error);
}
},
// string key and object value
testSetObjectValue = async function() {
try {
const result = await puter.kv.set('objectKey', { a: 1 });
assert(result === true, "Failed to set object as value");
const value = await puter.kv.get('objectKey');
assert(value.a === 1, "Failed to retrieve object value");
pass("testSetObjectValue passed");
} catch (error) {
fail("testSetObjectValue failed:", error);
}
},
// string key and array value
testSetArrayValue = async function() {
try {
const result = await puter.kv.set('arrayKey', [1, 2, 3]);
assert(result === true, "Failed to set array as value");
const value = await puter.kv.get('arrayKey');
assert(value[0] === 1, "Failed to retrieve array value");
pass("testSetArrayValue passed");
} catch (error) {
fail("testSetArrayValue failed:", error);
}
},
testSetKeyWithSpecialCharactersAndRetrieve = async function() {
try {
await puter.kv.set('special@Key#', 'value');
const value = await puter.kv.get('special@Key#');
assert(value === 'value', "Failed to retrieve value for key with special characters");
pass("testSetKeyWithSpecialCharactersAndRetrieve passed");
} catch (error) {
fail("testSetKeyWithSpecialCharactersAndRetrieve failed:", error);
}
},
testConcurrentSetOperations = async function() {
try {
const promises = [puter.kv.set('key1', 'value1'), puter.kv.set('key2', 'value2')];
const results = await Promise.all(promises);
assert(results.every(result => result === true), "Failed to set concurrent keys");
pass("testConcurrentSetOperations passed");
} catch (error) {
fail("testConcurrentSetOperations failed:", error);
}
},
//test flush: create a bunch of keys, flush, then check if they exist
testFlush = async function() {
try {
const keys = [];
for(let i = 0; i < 10; i++){
keys.push('key' + i);
}
await Promise.all(keys.map(key => puter.kv.set(key, 'value')));
await puter.kv.flush();
const results = await Promise.all(keys.map(key => puter.kv.get(key)));
assert(results.every(result => result === null), "Failed to flush keys");
pass("testFlush passed");
} catch (error) {
fail("testFlush failed:", error);
}
},
// incr
testIncr = async function() {
try {
const result = await puter.kv.incr('incrKey');
assert(result === 1, "Failed to increment key");
pass("testIncr passed");
} catch (error) {
fail("testIncr failed:", error);
}
},
// decr
testDecr = async function() {
try {
const result = await puter.kv.decr('decrKey');
assert(result === -1, "Failed to decrement key");
pass("testDecr passed");
} catch (error) {
fail("testDecr failed:", error);
}
},
// incr existing key
testIncrExistingKey = async function() {
try {
await puter.kv.set('incrKey', 1);
const result = await puter.kv.incr('incrKey');
assert(result === 2, "Failed to increment existing key");
pass("testIncrExistingKey passed");
} catch (error) {
fail("testIncrExistingKey failed:", error);
}
},
// decr existing key
testIncrExistingKey = async function() {
try {
await puter.kv.set('decrKey', 2);
const result = await puter.kv.decr('decrKey');
assert(result === 1, "Failed to decrement existing key");
pass("testDecrExistingKey passed");
} catch (error) {
fail("testDecrExistingKey failed:", error);
}
},
// incr by amount
testIncrByAmount = async function() {
try {
await puter.kv.set('incrKey', 1);
const result = await puter.kv.incr('incrKey', 5);
assert(result === 6, "Failed to increment key by amount");
pass("testIncrByAmount passed");
} catch (error) {
fail("testIncrByAmount failed:", error);
}
},
// decr by amount
testDecrByAmount = async function() {
try {
await puter.kv.set('decrKey', 10);
const result = await puter.kv.decr('decrKey', 5);
assert(result === 5, "Failed to decrement key by amount");
pass("testDecrByAmount passed");
} catch (error) {
fail("testDecrByAmount failed:", error);
}
},
// incr by amount existing key
testIncrByAmountExistingKey = async function() {
try {
await puter.kv.set('incrKey', 1);
const result = await puter.kv.incr('incrKey', 5);
assert(result === 6, "Failed to increment existing key by amount");
pass("testIncrByAmountExistingKey passed");
} catch (error) {
fail("testIncrByAmountExistingKey failed:", error);
}
},
// decr by amount existing key
testDecrByAmountExistingKey= async function() {
try {
await puter.kv.set('decrKey', 10);
const result = await puter.kv.decr('decrKey', 5);
assert(result === 5, "Failed to decrement existing key by amount");
pass("testDecrByAmountExistingKey passed");
} catch (error) {
fail("testDecrByAmountExistingKey failed:", error);
}
},
// incr by negative amount
testIncrByNegativeAmount = async function() {
try {
await puter.kv.set('incrKey', 1);
const result = await puter.kv.incr('incrKey', -5);
assert(result === -4, "Failed to increment key by negative amount");
pass("testIncrByNegativeAmount passed");
} catch (error) {
fail("testIncrByNegativeAmount failed:", error);
}
},
// decr by negative amount
testDecrByNegativeAmount = async function() {
try {
await puter.kv.set('decrKey', 10);
const result = await puter.kv.decr('decrKey', -5);
assert(result === 15, "Failed to decrement key by negative amount");
pass("testDecrByNegativeAmount passed");
} catch (error) {
fail("testDecrByNegativeAmount failed:", error);
}
},
// list keys
testListKeys = async function() {
try {
const keys = [];
// flush first
await puter.kv.flush();
// create 10 keys
for(let i = 0; i < 10; i++){
keys.push('key' + i);
}
// set all keys
await Promise.all(keys.map(key => puter.kv.set(key, 'value')));
// list keys
const result = await puter.kv.list();
assert(result.length === 10, "Failed to list keys");
pass("testListKeys passed");
} catch (error) {
fail("testListKeys failed:", error);
}
},
// list keys using glob
testListKeysGlob = async function() {
try {
const keys = [];
// flush first
await puter.kv.flush();
// create 10 keys
for(let i = 0; i < 10; i++){
keys.push('key' + i);
}
// set all keys
await Promise.all(keys.map(key => puter.kv.set(key, 'value')));
// list keys
const result = await puter.kv.list('k*');
assert(result.length === 10, "Failed to list keys using glob");
pass("testListKeysGlob passed");
} catch (error) {
fail("testListKeysGlob failed:", error);
}
},
]

View File

@ -0,0 +1,137 @@
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="../dist/puter.dev.js"></script>
<script src="./kv.test.js"></script>
<script src="./fs.test.js"></script>
<style>
#tests {
margin-top: 20px;
}
#run-tests {
margin-top: 20px;
margin-bottom: 20px;
background-color: #4c84af;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
cursor: pointer;
}
#unselect-all {
margin-left: 20px;
cursor: pointer;
}
#select-all {
margin-left: 20px;
cursor: pointer;
}
.test-container{
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", () => {
window.pass = function(msg) {
// $('#tests').append(`<p style="color:green;">${msg}</p>`);
}
window.fail = function(msg) {
throw new Error(msg);
}
// print the test name with checkbox for each test
$('#tests').append('<h2>File System Tests</h2>');
for (let i = 0; i < fsTests.length; i++) {
$('#tests').append(`<div class="test-container" id="fsTests-container-${i}">
<input type="checkbox" class="test-checkbox" id="fsTests${i}" checked>
<label for="fsTests${i}">${fsTests[i].name}</label><br>
</div>`);
}
$('#tests').append('<h2>Key Value Tests</h2>');
for (let i = 0; i < kvTests.length; i++) {
$('#tests').append(`<div class="test-container" id="kvTests-container-${i}">
<input type="checkbox" class="test-checkbox" id="kvTests${i}" checked>
<label for="kvTests${i}">${kvTests[i].name}</label><br>
</div>`);
}
window.assert = function(condition, message) {
if (!condition) {
throw new Error(message || "Assertion failed");
}
}
async function runTests() {
// go through fsTests and run each test
for (let i = 0; i < fsTests.length; i++) {
if (document.getElementById(`fsTests${i}`).checked) {
try{
await fsTests[i]();
// make this test's container green
$(`#fsTests-container-${i}`).css('background-color', '#85e085');
} catch (e) {
console.log(e);
// make this test's container red
$(`#fsTests-container-${i}`).css('background-color', '#ffbfbf');
// message
$(`#fsTests-container-${i}`).append(`<p style="color:#c00000;">${e}</p>`);
}
}
}
for (let i = 0; i < kvTests.length; i++) {
if (document.getElementById(`kvTests${i}`).checked) {
try{
await kvTests[i]();
// make this test's container green
$(`#kvTests-container-${i}`).css('background-color', '#85e085');
} catch (e) {
// make this test's container red
$(`#kvTests-container-${i}`).css('background-color', '#ff8484');
// message
$(`#kvTests-container-${i}`).append(`<p style="color:red;">${e}</p>`);
}
}
}
}
$('#run-tests').click(() => {
runTests();
});
$('#unselect-all').click(() => {
for (let i = 0; i < fsTests.length; i++) {
$('.test-checkbox').prop('checked', false);
}
});
$('#select-all').click(() => {
for (let i = 0; i < fsTests.length; i++) {
$('.test-checkbox').prop('checked', true);
}
});
});
</script>
</head>
<body>
<nav style="position: fixed; top: 0; width: 100%; background: #EEE; left: 0; padding-left: 10px;">
<button id="run-tests">Run Tests</button>
<span id="select-all">Select All</span>
<span id="unselect-all">Unselect All</span>
</nav>
<div id="tests" style="margin-top:100px;"></div>
</body>
</html>

View File

@ -53,11 +53,9 @@ window.gui = async function(options){
window.max_item_name_length = options.max_item_name_length ?? 500;
window.require_email_verification_to_publish_website = options.require_email_verification_to_publish_website ?? true;
// Add Puter.JS
await loadScript('https://js.puter.com/v2/');
// DEV: Load the initgui.js file if we are in development mode
if(!window.gui_env || window.gui_env === "dev"){
await loadScript('/sdk/puter.dev.js');
await loadScript('/initgui.js', {isModule: true});
}
@ -65,6 +63,7 @@ window.gui = async function(options){
// note: the order of the bundles is important
// note: Build script will prepend `window.gui_env="prod"` to the top of the file
else if(gui_env === "prod"){
await loadScript('https://js.puter.com/v2/');
// Load the minified bundles
await loadCSS('/dist/bundle.min.css');
await loadScript('/dist/bundle.min.js');