mirror of
https://github.com/glitternetwork/pinme.git
synced 2026-09-21 12:50:35 +08:00
Merge remote-tracking branch 'origin/main' into feat/support-car
This commit is contained in:
@@ -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
|
||||
|
||||
[](https://star-history.com/#glitternetwork/pinme&Date)
|
||||
|
||||
## Contact Us
|
||||
|
||||
If you have questions or suggestions, please contact us through:
|
||||
|
||||
+88
-61
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user