218 lines
6.9 KiB
TypeScript
218 lines
6.9 KiB
TypeScript
import chalk from 'chalk';
|
|
import inquirer from 'inquirer';
|
|
import figlet from 'figlet';
|
|
import { removeFromIpfs } from './utils/removeFromIpfs';
|
|
import tracker, { getTrackErrorReason } from './utils/tracker';
|
|
import {
|
|
TRACK_EVENTS,
|
|
TRACK_PAGES,
|
|
resolveTrackAction,
|
|
} from './utils/trackerEvents';
|
|
|
|
interface RemoveOptions {
|
|
[key: string]: any;
|
|
}
|
|
|
|
import { checkNodeVersion } from './utils/checkNodeVersion';
|
|
checkNodeVersion();
|
|
|
|
// Validate IPFS hash format
|
|
function isValidIPFSHash(hash: string): boolean {
|
|
// IPFS v0 hash (Qm...)
|
|
const v0Pattern = /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/;
|
|
// IPFS v1 hash (bafy...)
|
|
const v1Pattern = /^bafy[a-z2-7]{50,}$/;
|
|
// IPFS v1 hash (bafk...)
|
|
const v1kPattern = /^bafk[a-z2-7]{50,}$/;
|
|
// IPFS v1 hash (bafybe...)
|
|
const v1bePattern = /^bafybe[a-z2-7]{50,}$/;
|
|
|
|
return v0Pattern.test(hash) || v1Pattern.test(hash) || v1kPattern.test(hash) || v1bePattern.test(hash);
|
|
}
|
|
|
|
// Validate subname format (simple alphanumeric combination)
|
|
function isValidSubname(subname: string): boolean {
|
|
const subnamePattern = /^[a-zA-Z0-9]{6,12}$/;
|
|
return subnamePattern.test(subname);
|
|
}
|
|
|
|
// Parse different input formats
|
|
function parseInput(input: string): { type: 'hash' | 'subname'; value: string } | null {
|
|
const trimmedInput = input.trim();
|
|
|
|
// Case 1: Full URL with hash - https://bafybeigthbkdv2ufll47r7e7f5z4c3vubyggxwotl52parmy3d3abt6ztu.pinme.eth.limo
|
|
const hashUrlPattern = /https?:\/\/([a-z0-9]{50,})\.pinme\.dev/i;
|
|
const hashUrlMatch = trimmedInput.match(hashUrlPattern);
|
|
if (hashUrlMatch) {
|
|
const hash = hashUrlMatch[1];
|
|
if (isValidIPFSHash(hash)) {
|
|
return { type: 'hash', value: hash };
|
|
}
|
|
}
|
|
|
|
// Case 2: Direct hash - bafybeigthbkdv2ufll47r7e7f5z4c3vubyggxwotl52parmy3d3abt6ztu
|
|
if (isValidIPFSHash(trimmedInput)) {
|
|
return { type: 'hash', value: trimmedInput };
|
|
}
|
|
|
|
// Case 3: Subname URL - https://3abt6ztu.<root-domain>
|
|
const subnameUrlPattern = /https?:\/\/([a-zA-Z0-9]{6,12})\.[a-zA-Z0-9.-]+/i;
|
|
const subnameUrlMatch = trimmedInput.match(subnameUrlPattern);
|
|
if (subnameUrlMatch) {
|
|
const subname = subnameUrlMatch[1];
|
|
if (isValidSubname(subname)) {
|
|
return { type: 'subname', value: subname };
|
|
}
|
|
}
|
|
|
|
// Case 4: Direct subname - 3abt6ztu
|
|
if (isValidSubname(trimmedInput)) {
|
|
return { type: 'subname', value: trimmedInput };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export default async (options?: RemoveOptions): Promise<void> => {
|
|
try {
|
|
console.log(
|
|
figlet.textSync('PINME', {
|
|
font: 'Standard',
|
|
horizontalLayout: 'default',
|
|
verticalLayout: 'default',
|
|
width: 180,
|
|
whitespaceBreak: true,
|
|
}),
|
|
);
|
|
|
|
// If parameter is passed, remove directly, e.g.: pinme rm <hash>
|
|
const argHash = process.argv[3];
|
|
|
|
if (argHash && !argHash.startsWith('-')) {
|
|
const parsedInput = parseInput(argHash);
|
|
|
|
if (!parsedInput) {
|
|
console.log(chalk.red(`Invalid input format: ${argHash}`));
|
|
console.log(chalk.yellow('Supported formats:'));
|
|
console.log(chalk.yellow(' - IPFS hash: bafybeig...'));
|
|
console.log(chalk.yellow(' - Subname: 3abt6ztu'));
|
|
console.log(chalk.yellow(' - Subname URL: https://3abt6ztu.<root-domain>'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const success = await removeFromIpfs(parsedInput.value, parsedInput.type);
|
|
if (success) {
|
|
void tracker.trackEvent(TRACK_EVENTS.removeSuccess, TRACK_PAGES.remove, {
|
|
a: resolveTrackAction(TRACK_EVENTS.removeSuccess),
|
|
input_type: parsedInput.type,
|
|
});
|
|
console.log(
|
|
chalk.cyan(
|
|
figlet.textSync('Successful', { horizontalLayout: 'full' }),
|
|
),
|
|
);
|
|
}
|
|
} catch (error: any) {
|
|
void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, {
|
|
a: resolveTrackAction(TRACK_EVENTS.removeFailed),
|
|
input_type: parsedInput.type,
|
|
reason: getTrackErrorReason(error),
|
|
});
|
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Interactive removal
|
|
console.log(chalk.yellow('⚠️ Warning: This action will permanently remove the content from IPFS network'));
|
|
console.log(chalk.yellow('⚠️ Make sure you have the correct IPFS hash'));
|
|
console.log('');
|
|
|
|
const confirmAnswer = await inquirer.prompt([
|
|
{
|
|
type: 'confirm',
|
|
name: 'confirm',
|
|
message: 'Do you want to continue?',
|
|
default: false,
|
|
},
|
|
]);
|
|
|
|
if (!confirmAnswer.confirm) {
|
|
console.log(chalk.yellow('Operation cancelled'));
|
|
return;
|
|
}
|
|
|
|
const answer = await inquirer.prompt([
|
|
{
|
|
type: 'input',
|
|
name: 'input',
|
|
message: 'Enter IPFS hash, subname, or URL to remove:',
|
|
validate: (input: string) => {
|
|
if (!input.trim()) {
|
|
return 'Please enter an IPFS hash, subname, or URL';
|
|
}
|
|
const parsedInput = parseInput(input.trim());
|
|
if (!parsedInput) {
|
|
return 'Invalid format. Supported: IPFS hash, full URL (*), subname, or subname URL (*.<root-domain>)';
|
|
}
|
|
return true;
|
|
},
|
|
},
|
|
]);
|
|
|
|
if (answer.input) {
|
|
const parsedInput = parseInput(answer.input.trim());
|
|
|
|
if (!parsedInput) {
|
|
console.log(chalk.red('Invalid input format'));
|
|
return;
|
|
}
|
|
|
|
// Final confirmation
|
|
const finalConfirm = await inquirer.prompt([
|
|
{
|
|
type: 'confirm',
|
|
name: 'confirm',
|
|
message: `Are you sure you want to remove ${parsedInput.type === 'hash' ? 'hash' : 'subname'}: ${parsedInput.value}?`,
|
|
default: false,
|
|
},
|
|
]);
|
|
|
|
if (!finalConfirm.confirm) {
|
|
console.log(chalk.yellow('Operation cancelled'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const success = await removeFromIpfs(parsedInput.value, parsedInput.type);
|
|
if (success) {
|
|
void tracker.trackEvent(TRACK_EVENTS.removeSuccess, TRACK_PAGES.remove, {
|
|
a: resolveTrackAction(TRACK_EVENTS.removeSuccess),
|
|
input_type: parsedInput.type,
|
|
});
|
|
console.log(
|
|
chalk.cyan(
|
|
figlet.textSync('Successful', { horizontalLayout: 'full' }),
|
|
),
|
|
);
|
|
}
|
|
} catch (error: any) {
|
|
void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, {
|
|
a: resolveTrackAction(TRACK_EVENTS.removeFailed),
|
|
input_type: parsedInput.type,
|
|
reason: getTrackErrorReason(error),
|
|
});
|
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, {
|
|
a: resolveTrackAction(TRACK_EVENTS.removeFailed),
|
|
reason: getTrackErrorReason(error),
|
|
});
|
|
console.error(chalk.red(`Error executing remove command: ${error.message}`));
|
|
console.error(error.stack);
|
|
}
|
|
};
|