diff --git a/README.md b/README.md index 7fdae9b..27ab2fe 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ PinMe handles availability and persistence for you. Website: [https://pinme.eth.limo/](https://pinme.eth.limo/) - ## Installation ### Using npm @@ -106,6 +105,23 @@ pinme set-appkey pinme set-appkey ``` +### View AppKey information + +```bash +# Show current AppKey (masked for security) +pinme show-appkey + +# Or use the shorthand command +pinme appkey +``` + +### Log out + +```bash +# Log out and clear authentication +pinme logout +``` + ### View your domains ```bash @@ -134,10 +150,12 @@ pinme upload [path] [--domain ] ``` **Options:** + - `path`: Path to the file or directory to upload (optional, if not provided, interactive mode will be entered) - `-d, --domain `: Pinme subdomain to bind after upload (optional) **Examples:** + ```bash # Interactive upload pinme upload @@ -193,9 +211,11 @@ pinme rm [hash] ``` **Options:** + - `hash`: IPFS content hash to remove (optional, if not provided, interactive mode will be entered) **Examples:** + ```bash # Interactive removal pinme rm @@ -216,10 +236,12 @@ pinme ls [options] ``` **Options:** + - `-l, --limit `: Limit the number of records displayed - `-c, --clear`: Clear all upload history **Examples:** + ```bash # Show the last 10 records pinme list @@ -240,9 +262,11 @@ pinme set-appkey [AppKey] ``` **Options:** + - `AppKey`: Your AppKey for authentication (optional, if not provided, interactive mode will be entered) **Examples:** + ```bash # Interactive AppKey setup pinme set-appkey @@ -253,6 +277,55 @@ pinme set-appkey your-app-key-here **Note:** After setting the AppKey, your anonymous upload history will be automatically merged to your account. +### `show-appkey` / `appkey` + +Display current AppKey information with masked sensitive data. + +```bash +pinme show-appkey +pinme appkey +``` + +**Description:** + +This command shows the current AppKey information including: +- Address (fully displayed) +- Token (masked for security) +- AppKey (masked for security) + +**Examples:** + +```bash +# Show AppKey information +pinme show-appkey + +# Shorthand command +pinme appkey +``` + +**Note:** Sensitive information (token and AppKey) will be masked to protect your credentials. Only the address is fully displayed. + +### `logout` + +Log out and clear authentication information from local storage. + +```bash +pinme logout +``` + +**Description:** + +This command logs out the current user and removes the authentication information from local storage. After logging out, you will need to set your AppKey again to use authenticated features. + +**Examples:** + +```bash +# Log out +pinme logout +``` + +**Note:** This action will remove your AppKey from local storage. You can set it again using `pinme set-appkey` command. + ### `my-domains` / `domain` List all domains owned by the current account. @@ -263,6 +336,7 @@ pinme domain ``` **Examples:** + ```bash # List all domains pinme my-domains @@ -272,6 +346,7 @@ pinme domain ``` This command displays information about each domain including: + - Domain name - Domain type - Bind time @@ -286,9 +361,11 @@ pinme help [command] ``` **Options:** + - `command`: The specific command to view help for (optional) **Examples:** + ```bash # Display general help pinme help @@ -309,10 +386,10 @@ Uploaded files are stored on the IPFS network and accessible through the Glitter ### Log Locations Logs and configuration files are stored in: + - Linux/macOS: `~/.pinme/` - Windows: `%USERPROFILE%\.pinme\` - ## License MIT License - See the [LICENSE](LICENSE) file for details @@ -328,9 +405,9 @@ When uploading projects built with Vite, please note: ```js // vite.config.js export default { - base: "./", + base: './', // other configurations... -} +}; ``` ### Working with CAR Files @@ -349,10 +426,12 @@ PinMe can be integrated with GitHub Actions to automatically deploy your project ### Quick Setup 1. **Add the workflow file** to your repository: + - Copy `.github/workflows/deploy.yml` from the PinMe repository to your project - Or create `.github/workflows/deploy.yml` in your repository 2. **Configure GitHub Secrets**: + - Go to your repository → Settings → Secrets and variables → Actions - Add a new secret named `PINME_APPKEY` with your PinMe AppKey - (Optional) Add `PINME_DOMAIN` to specify a custom domain name @@ -380,6 +459,7 @@ The GitHub Actions workflow automatically: You can configure the following secrets in your repository: - **`PINME_APPKEY`** (Required): Your PinMe AppKey for authentication + - Format: `
-` - Get your AppKey from [PinMe website](https://pinme.eth.limo/) @@ -444,18 +524,25 @@ The workflow automatically detects and supports: ### Troubleshooting **Build directory not found:** + - Ensure your build script outputs to a standard directory (`dist`, `build`, `public`, or `out`) - Or set `PINME_DOMAIN` secret and use manual workflow dispatch to specify custom directory **Authentication failed:** + - Verify your `PINME_APPKEY` secret is correctly set - Ensure the AppKey format is correct: `
-` **Domain binding failed:** + - Check if the domain name is available - Ensure you have permission to bind the domain - Try a different domain name +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=glitternetwork/pinme&type=Date)](https://star-history.com/#glitternetwork/pinme&Date) + ## Contact Us If you have questions or suggestions, please contact us through: diff --git a/bin/index.ts b/bin/index.ts index 452dd66..634ebeb 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -5,9 +5,9 @@ dotenv.config(); import { checkNodeVersion } from './utils/checkNodeVersion'; checkNodeVersion(); -import { Command } from "commander"; -import chalk from "chalk"; -import figlet from "figlet"; +import { Command } from 'commander'; +import chalk from 'chalk'; +import figlet from 'figlet'; import { version } from '../package.json'; import upload from './upload'; @@ -15,93 +15,116 @@ import importFile from './import'; import remove from './remove'; import { displayUploadHistory, clearUploadHistory } from './utils/history'; import setAppKeyCmd from './set-appkey'; +import logoutCmd from './logout'; +import showAppKeyCmd from './show-appkey'; import myDomainsCmd from './my-domains'; // display the ASCII art logo function showBanner(): void { console.log( - chalk.cyan( - figlet.textSync("Pinme", { horizontalLayout: "full" }) - ) + chalk.cyan(figlet.textSync('Pinme', { horizontalLayout: 'full' })), ); - console.log(chalk.cyan("A command-line tool for uploading files to IPFS\n")); + console.log(chalk.cyan('A command-line tool for uploading files to IPFS\n')); } const program = new Command(); program - .name("pinme") - .version(version) - .option('-v, --version', 'output the current version'); + .name('pinme') + .version(version) + .option('-v, --version', 'output the current version'); program - .command('upload') - .description("upload a file or directory to IPFS. Supports --domain to bind after upload") - .option('-d, --domain ', 'Pinme subdomain') - .action(() => upload()); + .command('upload') + .description( + 'upload a file or directory to IPFS. Supports --domain to bind after upload', + ) + .option('-d, --domain ', 'Pinme subdomain') + .action(() => upload()); program - .command('import') - .description("import a CAR file to IPFS. Supports --domain to bind after import") - .option('-d, --domain ', 'Pinme subdomain') - .action(() => importFile()); + .command('import') + .description("import a CAR file to IPFS. Supports --domain to bind after import") + .option('-d, --domain ', 'Pinme subdomain') + .action(() => importFile()); program - .command('rm') - .description("remove a file from IPFS network") - .action(() => remove()); + .command('rm') + .description('remove a file from IPFS network') + .action(() => remove()); program - .command('set-appkey') - .description("Set AppKey for authentication, and auto-merge anonymous history") - .action(() => setAppKeyCmd()); + .command('set-appkey') + .description( + 'Set AppKey for authentication, and auto-merge anonymous history', + ) + .action(() => setAppKeyCmd()); program - .command('my-domains') - .alias('domain') - .description("List domains owned by current account") - .action(() => myDomainsCmd()); + .command('logout') + .description('log out and clear authentication') + .action(() => logoutCmd()); program - .command('domain') - .description("Alias for 'my-domains' command") - .action(() => myDomainsCmd()); + .command('show-appkey') + .alias('appkey') + .description('show current AppKey information (masked)') + .action(() => showAppKeyCmd()); program - .command('list') - .description("show upload history") - .option('-l, --limit ', 'limit the number of records to show', parseInt) - .option('-c, --clear', 'clear all upload history') - .action((options: { limit?: number, clear?: boolean }) => { - if (options.clear) { - clearUploadHistory(); - } else { - displayUploadHistory(options.limit || 10); - } - }); + .command('my-domains') + .alias('domain') + .description('List domains owned by current account') + .action(() => myDomainsCmd()); + +program + .command('domain') + .description("Alias for 'my-domains' command") + .action(() => myDomainsCmd()); + +program + .command('list') + .description('show upload history') + .option( + '-l, --limit ', + 'limit the number of records to show', + parseInt, + ) + .option('-c, --clear', 'clear all upload history') + .action((options: { limit?: number; clear?: boolean }) => { + if (options.clear) { + clearUploadHistory(); + } else { + displayUploadHistory(options.limit || 10); + } + }); // add ls command as an alias for list command program - .command('ls') - .description("alias for 'list' command") - .option('-l, --limit ', 'limit the number of records to show', parseInt) - .option('-c, --clear', 'clear all upload history') - .action((options: { limit?: number, clear?: boolean }) => { - if (options.clear) { - clearUploadHistory(); - } else { - displayUploadHistory(options.limit || 10); - } - }); + .command('ls') + .description("alias for 'list' command") + .option( + '-l, --limit ', + 'limit the number of records to show', + parseInt, + ) + .option('-c, --clear', 'clear all upload history') + .action((options: { limit?: number; clear?: boolean }) => { + if (options.clear) { + clearUploadHistory(); + } else { + displayUploadHistory(options.limit || 10); + } + }); // add help command program - .command('help') - .description("display help information") - .action(() => { - showBanner(); - program.help(); - }); + .command('help') + .description('display help information') + .action(() => { + showBanner(); + program.help(); + }); // custom help output format program.on('--help', () => { @@ -113,13 +136,17 @@ program.on('--help', () => { console.log(' $ pinme import --domain '); console.log(' $ pinme rm '); console.log(' $ pinme set-appkey '); + console.log(' $ pinme show-appkey'); + console.log(' $ pinme logout'); console.log(' $ pinme my-domains'); console.log(' $ pinme domain'); console.log(' $ pinme list -l 5'); console.log(' $ pinme ls'); console.log(' $ pinme help'); console.log(''); - console.log('For more information, visit: https://github.com/glitternetwork/pinme'); + console.log( + 'For more information, visit: https://github.com/glitternetwork/pinme', + ); }); // parse the command line arguments @@ -129,4 +156,4 @@ program.parse(process.argv); if (process.argv.length === 2) { showBanner(); program.help(); -} \ No newline at end of file +} diff --git a/bin/logout.ts b/bin/logout.ts new file mode 100644 index 0000000..1100752 --- /dev/null +++ b/bin/logout.ts @@ -0,0 +1,36 @@ +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import { clearAuthToken, getAuthConfig } from './utils/auth'; + +export default async function logoutCmd(): Promise { + try { + // Check if user is logged in + const auth = getAuthConfig(); + if (!auth) { + console.log(chalk.yellow('No active session found. You are already logged out.')); + return; + } + + // Confirm logout + const answer = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: `Are you sure you want to log out? (Current address: ${auth.address})`, + default: false, + }, + ]); + + if (!answer.confirm) { + console.log(chalk.blue('Logout cancelled.')); + return; + } + + // Clear auth token + clearAuthToken(); + console.log(chalk.green('Successfully logged out.')); + console.log(chalk.gray(`Address ${auth.address} has been removed from local storage.`)); + } catch (e: any) { + console.log(chalk.red(`Failed to logout: ${e?.message || e}`)); + } +} diff --git a/bin/show-appkey.ts b/bin/show-appkey.ts new file mode 100644 index 0000000..caceb0a --- /dev/null +++ b/bin/show-appkey.ts @@ -0,0 +1,39 @@ +import chalk from 'chalk'; +import { getAuthConfig } from './utils/auth'; + +export default function showAppKeyCmd(): void { + try { + const auth = getAuthConfig(); + if (!auth) { + console.log(chalk.yellow('No AppKey found. Please set your AppKey first.')); + console.log(chalk.gray('Run: pinme set-appkey ')); + return; + } + + // Display address (safe to show) + console.log(chalk.green('Current AppKey Information:')); + console.log(chalk.cyan(` Address: ${auth.address}`)); + + // Show token with masking (show first 8 chars and last 4 chars) + const token = auth.token; + if (token.length > 12) { + const maskedToken = `${token.substring(0, 8)}${'*'.repeat(token.length - 12)}${token.substring(token.length - 4)}`; + console.log(chalk.cyan(` Token: ${maskedToken}`)); + } else { + // If token is too short, just show asterisks + console.log(chalk.cyan(` Token: ${'*'.repeat(token.length)}`)); + } + + // Show full combined format (AppKey) + const combined = `${auth.address}-${auth.token}`; + if (combined.length > 20) { + const maskedAppKey = `${combined.substring(0, 12)}${'*'.repeat(combined.length - 16)}${combined.substring(combined.length - 4)}`; + console.log(chalk.cyan(` AppKey: ${maskedAppKey}`)); + } else { + console.log(chalk.cyan(` AppKey: ${'*'.repeat(combined.length)}`)); + } + } catch (e: any) { + console.log(chalk.red(`Failed to show AppKey: ${e?.message || e}`)); + } +} + diff --git a/bin/upload.ts b/bin/upload.ts index ea9c9e4..9bb7e0d 100644 --- a/bin/upload.ts +++ b/bin/upload.ts @@ -6,8 +6,7 @@ import upload from './utils/uploadToIpfsSplit'; import fs from 'fs'; import CryptoJS from 'crypto-js'; import { checkDomainAvailable, bindPinmeDomain } from './utils/pinmeApi'; -import { getAuthConfig } from './utils/auth'; -import { getDeviceId } from './utils/getDeviceId'; +import { getUid } from './utils/getDeviceId'; // get from environment variables const URL = process.env.IPFS_PREVIEW_URL; @@ -17,7 +16,11 @@ import { checkNodeVersion } from './utils/checkNodeVersion'; checkNodeVersion(); // encrypt the hash with optional uid (device id) -function encryptHash(contentHash: string, key: string | undefined, uid?: string): string { +function encryptHash( + contentHash: string, + key: string | undefined, + uid?: string, +): string { try { if (!key) { throw new Error('Secret key not found'); @@ -66,15 +69,6 @@ function getDomainFromArgs(): string | null { return null; } -// Get uid: use address from auth if logged in, otherwise use deviceId -function getUid(): string { - const auth = getAuthConfig(); - if (auth?.address) { - return auth.address; - } - return getDeviceId(); -} - export default async (options?: UploadOptions): Promise => { try { console.log( @@ -103,7 +97,11 @@ export default async (options?: UploadOptions): Promise => { if (domainArg) { const check = await checkDomainAvailable(domainArg); if (!check.is_valid) { - console.log(chalk.red(`Domain not available: ${check.error || 'unknown reason'}`)); + console.log( + chalk.red( + `Domain not available: ${check.error || 'unknown reason'}`, + ), + ); return; } console.log(chalk.green(`Domain available: ${domainArg}`)); @@ -124,11 +122,19 @@ export default async (options?: UploadOptions): Promise => { console.log(chalk.cyan(`${URL}${encryptedCID}`)); // optional: bind domain after upload if (domainArg) { - console.log(chalk.blue(`Binding domain: ${domainArg} with CID: ${result.contentHash}`)); + console.log( + chalk.blue( + `Binding domain: ${domainArg} with CID: ${result.contentHash}`, + ), + ); const ok = await bindPinmeDomain(domainArg, result.contentHash); if (ok) { console.log(chalk.green(`Bind success: ${domainArg}`)); - console.log(chalk.white(`Visit (Pinme subdomain example): https://${domainArg}.pinit.eth.limo`)); + console.log( + chalk.white( + `Visit (Pinme subdomain example): https://${domainArg}.pinit.eth.limo`, + ), + ); } else { console.log(chalk.red('Binding failed. Please try again later.')); } @@ -161,7 +167,11 @@ export default async (options?: UploadOptions): Promise => { if (domainArg) { const check = await checkDomainAvailable(domainArg); if (!check.is_valid) { - console.log(chalk.red(`Domain not available: ${check.error || 'unknown reason'}`)); + console.log( + chalk.red( + `Domain not available: ${check.error || 'unknown reason'}`, + ), + ); return; } console.log(chalk.green(`Domain available: ${domainArg}`)); @@ -182,11 +192,19 @@ export default async (options?: UploadOptions): Promise => { console.log(chalk.cyan(`URL:`)); console.log(chalk.cyan(`${URL}${encryptedCID}`)); if (domainArg) { - console.log(chalk.blue(`Binding domain: ${domainArg} with CID: ${result.contentHash}`)); + console.log( + chalk.blue( + `Binding domain: ${domainArg} with CID: ${result.contentHash}`, + ), + ); const ok = await bindPinmeDomain(domainArg, result.contentHash); if (ok) { console.log(chalk.green(`Bind success: ${domainArg}`)); - console.log(chalk.white(`Visit (Pinme subdomain example): https://${domainArg}.pinit.eth.limo`)); + console.log( + chalk.white( + `Visit (Pinme subdomain example): https://${domainArg}.pinit.eth.limo`, + ), + ); } else { console.log(chalk.red('Binding failed. Please try again later.')); } diff --git a/bin/utils/auth.ts b/bin/utils/auth.ts index ea55d1e..8f5a663 100644 --- a/bin/utils/auth.ts +++ b/bin/utils/auth.ts @@ -38,6 +38,16 @@ export function setAuthToken(combined: string): AuthConfig { return auth; } +export function clearAuthToken(): void { + try { + if (fs.existsSync(AUTH_FILE)) { + fs.removeSync(AUTH_FILE); + } + } catch (error) { + console.error(`Failed to clear auth token: ${error}`); + } +} + export function getAuthConfig(): AuthConfig | null { try { if (!fs.existsSync(AUTH_FILE)) return null; @@ -59,5 +69,3 @@ export function getAuthHeaders(): Record { 'authentication-tokens': conf.token, }; } - - diff --git a/bin/utils/getDeviceId.ts b/bin/utils/getDeviceId.ts index a60184e..28c1d00 100644 --- a/bin/utils/getDeviceId.ts +++ b/bin/utils/getDeviceId.ts @@ -2,20 +2,29 @@ import fs from 'fs-extra'; import path from 'path'; import os from 'os'; import { v4 as uuidv4 } from 'uuid'; +import { getAuthConfig } from './auth'; export function getDeviceId(): string { const configDir = path.join(os.homedir(), '.pinme'); const configFile = path.join(configDir, 'device-id'); - + if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } - + if (fs.existsSync(configFile)) { return fs.readFileSync(configFile, 'utf8').trim(); } - + const deviceId = uuidv4(); fs.writeFileSync(configFile, deviceId); return deviceId; -} \ No newline at end of file +} +// Get uid: use address from auth if logged in, otherwise use deviceId +export function getUid(): string { + const auth = getAuthConfig(); + if (auth?.address) { + return auth.address; + } + return getDeviceId(); +} diff --git a/bin/utils/help.ts b/bin/utils/help.ts index f7ff88a..0b1b1ea 100644 --- a/bin/utils/help.ts +++ b/bin/utils/help.ts @@ -22,6 +22,10 @@ function showGeneralHelp(): void { console.log(" upload Upload a file or directory to IPFS"); console.log(" list Show upload history"); console.log(" ls Alias for 'list' command"); + console.log(" set-appkey Set AppKey for authentication"); + console.log(" show-appkey Show current AppKey information (masked)"); + console.log(" appkey Alias for 'show-appkey' command"); + console.log(" logout Log out and clear authentication"); console.log(" help [command] Show help for a specific command\n"); console.log("OPTIONS:"); @@ -89,6 +93,41 @@ function showLsHelp(): void { console.log(" pinme ls -c"); } +// show-appkey command help +function showShowAppKeyHelp(): void { + console.log("COMMAND:"); + console.log(" show-appkey - Show current AppKey information (masked)\n"); + + console.log("USAGE:"); + console.log(" pinme show-appkey"); + console.log(" pinme appkey\n"); + + console.log("DESCRIPTION:"); + console.log(" This command displays the current AppKey information."); + console.log(" Sensitive information (token and AppKey) will be masked for security.\n"); + + console.log("EXAMPLES:"); + console.log(" pinme show-appkey"); + console.log(" pinme appkey"); +} + +// logout command help +function showLogoutHelp(): void { + console.log("COMMAND:"); + console.log(" logout - Log out and clear authentication\n"); + + console.log("USAGE:"); + console.log(" pinme logout\n"); + + console.log("DESCRIPTION:"); + console.log(" This command logs out the current user and clears the authentication"); + console.log(" information from local storage. You will need to set AppKey again"); + console.log(" to use authenticated features.\n"); + + console.log("EXAMPLES:"); + console.log(" pinme logout"); +} + // show the help for the command function showHelp(command?: string): void { if (!command) { @@ -106,6 +145,13 @@ function showHelp(command?: string): void { case 'ls': showLsHelp(); break; + case 'show-appkey': + case 'appkey': + showShowAppKeyHelp(); + break; + case 'logout': + showLogoutHelp(); + break; default: console.log(`Unknown command: ${command}`); showGeneralHelp(); diff --git a/bin/utils/removeFromIpfs.ts b/bin/utils/removeFromIpfs.ts index 964fb03..c4817d1 100644 --- a/bin/utils/removeFromIpfs.ts +++ b/bin/utils/removeFromIpfs.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import chalk from 'chalk'; -import { getDeviceId } from './getDeviceId'; +import { getUid } from './getDeviceId'; // Get API base URL from environment variables const ipfsApiUrl = @@ -18,65 +18,88 @@ interface RemoveResponse { * @param type - Type of the value: 'hash' or 'subname' * @returns Promise - Whether deletion was successful */ -export async function removeFromIpfs(value: string, type: 'hash' | 'subname' = 'hash'): Promise { +export async function removeFromIpfs( + value: string, + type: 'hash' | 'subname' = 'hash', +): Promise { try { - const uid = getDeviceId(); - + const uid = getUid(); + console.log(chalk.blue(`Removing content from IPFS: ${value}...`)); - + // Build query parameters based on type const queryParams = new URLSearchParams({ - uid: uid + uid: uid, }); - + if (type === 'subname') { queryParams.append('subname', value); } else { queryParams.append('arg', value); } - - const response = await axios.post(`${ipfsApiUrl}/block/rm?${queryParams.toString()}`, { - timeout: 30000 // 30 seconds timeout - }); + + const response = await axios.post( + `${ipfsApiUrl}/block/rm?${queryParams.toString()}`, + { + timeout: 30000, // 30 seconds timeout + }, + ); const { code, msg, data } = response.data; - + if (code === 200) { console.log(chalk.green('✓ Removal successful!')); - console.log(chalk.cyan(`Content ${type}: ${value} has been removed from IPFS network`)); + console.log( + chalk.cyan( + `Content ${type}: ${value} has been removed from IPFS network`, + ), + ); return true; } else { console.log(chalk.red('✗ Removal failed')); console.log(chalk.red(`Error: ${msg || 'Unknown error occurred'}`)); return false; } - } catch (error: any) { console.log(chalk.red('✗ Removal failed', error)); - + if (error.response) { // Server responded with error status code const { status, data } = error.response; - console.log(chalk.red(`HTTP Error ${status}: ${data?.msg || 'Server error'}`)); - + console.log( + chalk.red(`HTTP Error ${status}: ${data?.msg || 'Server error'}`), + ); + if (status === 404) { - console.log(chalk.yellow('Content not found on the network or already removed')); + console.log( + chalk.yellow('Content not found on the network or already removed'), + ); } else if (status === 403) { - console.log(chalk.yellow('Permission denied - you may not have access to remove this content')); + console.log( + chalk.yellow( + 'Permission denied - you may not have access to remove this content', + ), + ); } else if (status === 500) { - console.log(chalk.yellow('Server internal error - please try again later')); + console.log( + chalk.yellow('Server internal error - please try again later'), + ); } } else if (error.request) { // Request was made but no response received - console.log(chalk.red('Network error: Unable to connect to IPFS service')); - console.log(chalk.yellow('Please check your internet connection and try again')); + console.log( + chalk.red('Network error: Unable to connect to IPFS service'), + ); + console.log( + chalk.yellow('Please check your internet connection and try again'), + ); } else { // Other errors console.log(chalk.red(`Error: ${error.message}`)); } - + return false; } } -export default removeFromIpfs; \ No newline at end of file +export default removeFromIpfs; diff --git a/bin/utils/uploadToIpfs.ts b/bin/utils/uploadToIpfs.ts index b3d1aea..eefa073 100644 --- a/bin/utils/uploadToIpfs.ts +++ b/bin/utils/uploadToIpfs.ts @@ -10,7 +10,7 @@ import { formatSize, } from './uploadLimits'; import { saveUploadHistory } from './history'; -import { getDeviceId } from './getDeviceId'; +import { getUid } from './getDeviceId'; const ipfsApiUrl = process.env.IPFS_API_URL || 'https://ipfs.glitterprotocol.dev/api/v2'; @@ -189,8 +189,9 @@ function handleMultipartError(error: any, context: string): string { // error code const ERROR_CODES = { '30001': `File too large, single file max size: ${process.env.FILE_SIZE_LIMIT}MB,single folder max size: ${process.env.DIRECTORY_SIZE_LIMIT}MB`, - '30002': `Max storage quorum ${Number(process.env.STORAGE_SIZE_LIMIT) / 1000 - } GB reached`, + '30002': `Max storage quorum ${ + Number(process.env.STORAGE_SIZE_LIMIT) / 1000 + } GB reached`, }; function loadFilesToArrRecursively( @@ -892,7 +893,7 @@ export default async function (filePath: string): Promise<{ shortUrl?: string; } | null> { // check if the file is a directory - const deviceId = getDeviceId(); + const deviceId = getUid(); if (!deviceId) { throw new Error('Device ID not found'); } diff --git a/bin/utils/uploadToIpfsSplit.ts b/bin/utils/uploadToIpfsSplit.ts index 88a0841..c4cd5a9 100644 --- a/bin/utils/uploadToIpfsSplit.ts +++ b/bin/utils/uploadToIpfsSplit.ts @@ -10,18 +10,20 @@ import { formatSize, } from './uploadLimits'; import { saveUploadHistory } from './history'; -import { getDeviceId } from './getDeviceId'; +import { getUid } from './getDeviceId'; // Configuration constants -const IPFS_API_URL = process.env.IPFS_API_URL || 'https://ipfs.glitterprotocol.dev/api/v2'; +const IPFS_API_URL = + process.env.IPFS_API_URL || 'https://ipfs.glitterprotocol.dev/api/v2'; const MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '2'); const RETRY_DELAY = parseInt(process.env.RETRY_DELAY_MS || '1000'); const TIMEOUT = parseInt(process.env.TIMEOUT_MS || '600000'); -const MAX_POLL_TIME = parseInt(process.env.MAX_POLL_TIME_MINUTES || '5') * 60 * 1000; +const MAX_POLL_TIME = + parseInt(process.env.MAX_POLL_TIME_MINUTES || '5') * 60 * 1000; const POLL_INTERVAL = parseInt(process.env.POLL_INTERVAL_SECONDS || '2') * 1000; const PROGRESS_UPDATE_INTERVAL = 200; // ms const EXPECTED_UPLOAD_TIME = 60000; // 60 seconds -const MAX_PROGRESS = 0.90; // 90% +const MAX_PROGRESS = 0.9; // 90% // Type definitions interface ChunkSessionResponse { @@ -87,7 +89,7 @@ class StepProgressBar { this.spinner = ora(`Preparing to upload ${fileName}...`).start(); this.startTime = Date.now(); this.stepStartTime = Date.now(); - + this.startProgress(); } @@ -124,7 +126,9 @@ class StepProgressBar { this.stopProgress(); const totalTime = Math.floor((Date.now() - this.startTime) / 1000); const progressBar = this.createProgressBar(1); - this.spinner.succeed(`Upload completed ${progressBar} 100% (${totalTime}s)`); + this.spinner.succeed( + `Upload completed ${progressBar} 100% (${totalTime}s)`, + ); } fail(error: string): void { @@ -137,19 +141,21 @@ class StepProgressBar { this.progressInterval = setInterval(() => { const elapsed = Date.now() - this.startTime; let progress: number; - + if (this.isSimulatingProgress) { // Simulate progress after 90%, gradually grow from 90% to 99% const simulationElapsed = Date.now() - this.simulationStartTime; const simulationProgress = Math.min(simulationElapsed / 60000, 1); // From 90% to 99% within 60 seconds - progress = 0.90 + (simulationProgress * 0.09); // 90% + 9% = 99% + progress = 0.9 + simulationProgress * 0.09; // 90% + 9% = 99% } else { progress = this.calculateProgress(elapsed); } - + const duration = this.formatDuration(Math.floor(elapsed / 1000)); const progressBar = this.createProgressBar(progress); - this.spinner.text = `Uploading ${this.fileName}... ${progressBar} ${Math.round(progress * 100)}% (${duration})`; + this.spinner.text = `Uploading ${ + this.fileName + }... ${progressBar} ${Math.round(progress * 100)}% (${duration})`; }, PROGRESS_UPDATE_INTERVAL); } @@ -161,7 +167,10 @@ class StepProgressBar { } private calculateProgress(elapsed: number): number { - return Math.min((elapsed / EXPECTED_UPLOAD_TIME) * MAX_PROGRESS, MAX_PROGRESS); + return Math.min( + (elapsed / EXPECTED_UPLOAD_TIME) * MAX_PROGRESS, + MAX_PROGRESS, + ); } private createProgressBar(progress: number, width: number = 20): string { @@ -330,14 +339,19 @@ async function uploadChunkWithAbort( retryCount + 1, ); } - + throw new Error( - `Chunk ${chunkIndex + 1} upload failed after ${MAX_RETRIES} retries: ${error.message}`, + `Chunk ${chunkIndex + 1} upload failed after ${MAX_RETRIES} retries: ${ + error.message + }`, ); } } -async function delayWithAbortCheck(delay: number, signal: AbortSignal): Promise { +async function delayWithAbortCheck( + delay: number, + signal: AbortSignal, +): Promise { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { if (signal.aborted) { @@ -391,7 +405,7 @@ async function uploadFileChunks( chunkIndex, chunkData, deviceId, - abortController.signal + abortController.signal, ); if (abortController.signal.aborted) return; @@ -404,7 +418,9 @@ async function uploadFileChunks( } hasFatalError = true; - fatalError = `Chunk ${chunkIndex + 1}/${totalChunks} upload failed: ${error.message}`; + fatalError = `Chunk ${chunkIndex + 1}/${totalChunks} upload failed: ${ + error.message + }`; abortController.abort(); throw new Error(fatalError); } @@ -412,12 +428,16 @@ async function uploadFileChunks( }); try { - const results = await Promise.allSettled(uploadTasks.map(task => task())); - const failedResults = results.filter(result => result.status === 'rejected'); + const results = await Promise.allSettled(uploadTasks.map((task) => task())); + const failedResults = results.filter( + (result) => result.status === 'rejected', + ); if (failedResults.length > 0) { const firstFailure = failedResults[0] as PromiseRejectedResult; - throw new Error(firstFailure.reason.message || 'Error occurred during upload'); + throw new Error( + firstFailure.reason.message || 'Error occurred during upload', + ); } if (hasFatalError) { @@ -523,7 +543,7 @@ async function monitorChunkProgress( } } - await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL)); + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL)); } const maxPollTimeMinutes = Math.floor(MAX_POLL_TIME / (60 * 1000)); @@ -545,7 +565,9 @@ async function uploadDirectoryInChunks( const sizeCheck = checkDirectorySizeLimit(directoryPath); if (sizeCheck.exceeds) { throw new Error( - `Directory ${directoryPath} exceeds size limit ${formatSize(sizeCheck.limit)} (size: ${formatSize(sizeCheck.size)})`, + `Directory ${directoryPath} exceeds size limit ${formatSize( + sizeCheck.limit, + )} (size: ${formatSize(sizeCheck.size)})`, ); } @@ -618,7 +640,9 @@ async function uploadFileInChunks( const sizeCheck = checkFileSizeLimit(filePath); if (sizeCheck.exceeds) { throw new Error( - `File ${filePath} exceeds size limit ${formatSize(sizeCheck.limit)} (size: ${formatSize(sizeCheck.size)})`, + `File ${filePath} exceeds size limit ${formatSize( + sizeCheck.limit, + )} (size: ${formatSize(sizeCheck.size)})`, ); } @@ -680,7 +704,7 @@ export default async function (filePath: string, importAsCar: boolean = false): previewHash?: string | null; shortUrl?: string; } | null> { - const deviceId = getDeviceId(); + const deviceId = getUid(); if (!deviceId) { throw new Error('Device ID not found'); }