Merge remote-tracking branch 'origin/main' into feat/support-car

This commit is contained in:
rongbin666
2025-12-28 11:47:20 +08:00
11 changed files with 458 additions and 140 deletions
+91 -4
View File
@@ -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 <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 <name>]
```
**Options:**
- `path`: Path to the file or directory to upload (optional, if not provided, interactive mode will be entered)
- `-d, --domain <name>`: 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 <number>`: 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: `<address>-<jwt>`
- 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: `<address>-<jwt>`
**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:
+88 -61
View File
@@ -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 <name>', 'Pinme subdomain')
.action(() => upload());
.command('upload')
.description(
'upload a file or directory to IPFS. Supports --domain to bind after upload',
)
.option('-d, --domain <name>', 'Pinme subdomain')
.action(() => upload());
program
.command('import')
.description("import a CAR file to IPFS. Supports --domain to bind after import")
.option('-d, --domain <name>', 'Pinme subdomain')
.action(() => importFile());
.command('import')
.description("import a CAR file to IPFS. Supports --domain to bind after import")
.option('-d, --domain <name>', '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 <number>', '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 <number>',
'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 <number>', '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 <number>',
'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 <path> --domain <name>');
console.log(' $ pinme rm <hash>');
console.log(' $ pinme set-appkey <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();
}
}
+36
View File
@@ -0,0 +1,36 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { clearAuthToken, getAuthConfig } from './utils/auth';
export default async function logoutCmd(): Promise<void> {
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}`));
}
}
+39
View File
@@ -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 <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}`));
}
}
+36 -18
View File
@@ -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<void> => {
try {
console.log(
@@ -103,7 +97,11 @@ export default async (options?: UploadOptions): Promise<void> => {
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<void> => {
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<void> => {
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<void> => {
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.'));
}
+10 -2
View File
@@ -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<string, string> {
'authentication-tokens': conf.token,
};
}
+13 -4
View File
@@ -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;
}
}
// 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();
}
+46
View File
@@ -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();
+47 -24
View File
@@ -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<boolean> - Whether deletion was successful
*/
export async function removeFromIpfs(value: string, type: 'hash' | 'subname' = 'hash'): Promise<boolean> {
export async function removeFromIpfs(
value: string,
type: 'hash' | 'subname' = 'hash',
): Promise<boolean> {
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<RemoveResponse>(`${ipfsApiUrl}/block/rm?${queryParams.toString()}`, {
timeout: 30000 // 30 seconds timeout
});
const response = await axios.post<RemoveResponse>(
`${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;
export default removeFromIpfs;
+5 -4
View File
@@ -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');
}
+47 -23
View File
@@ -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<void> {
async function delayWithAbortCheck(
delay: number,
signal: AbortSignal,
): Promise<void> {
return new Promise<void>((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');
}