Initial commit for new repository

This commit is contained in:
junchi.zhang
2025-04-16 23:38:24 +08:00
commit 82b2e3bfa3
26 changed files with 4622 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
IPFS_API_URL=https://example.com
IPFS_PREVIEW_URL=https://example.com
SECRET_KEY=your-secret-key
FILE_SIZE_LIMIT=100 # MB
DIRECTORY_SIZE_LIMIT=500 # MB
+4
View File
@@ -0,0 +1,4 @@
node_modules
/dist
.DS_Store
.env
+10
View File
@@ -0,0 +1,10 @@
node_modules
/bin
/src
.github
.gitignore
.prettierrc
rollup.config.js
tsconfig.json
*.log
.env
+2
View File
@@ -0,0 +1,2 @@
/dist
*.yaml
+19
View File
@@ -0,0 +1,19 @@
module.exports = {
pluginSearchDirs: false,
plugins: [
require.resolve('prettier-plugin-organize-imports'),
require.resolve('prettier-plugin-packagejson'),
],
printWidth: 80,
proseWrap: 'never',
singleQuote: true,
trailingComma: 'all',
overrides: [
{
files: '*.md',
options: {
proseWrap: 'preserve',
},
},
],
};
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 PINME
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+161
View File
@@ -0,0 +1,161 @@
# PinMe
[PinMe](https://pinme.eth.limo/) is a simple and easy-to-use command-line tool for uploading files and directories to the [IPFS](https://ipfs.tech/) network.
## Features
- 🚀 Quickly upload files and directories to IPFS
- 📂 Support for various file types and sizes
- 📊 View and manage upload history
- 🔗 Automatically generate accessible IPFS links
- 🌐 Preview uploaded content
## Installation
### Using npm
```bash
npm install -g pinme
```
### Using yarn
```bash
yarn global add pinme
```
## Usage
### Upload files or directories
```bash
# Interactive upload
pinme upload
# Specify path directly
pinme upload /path/to/file-or-directory
```
### View upload history
```bash
# Show the last 10 upload records
pinme list
# Or use the shorthand command
pinme ls
# Limit the number of records shown
pinme list -l 5
# Clear all upload history
pinme list -c
```
### Get help
```bash
# Display help information
pinme help
```
## Command Details
### `upload`
Upload a file or directory to the IPFS network.
```bash
pinme upload [path]
```
**Options:**
- `path`: Path to the file or directory to upload (optional, if not provided, interactive mode will be entered)
**Examples:**
```bash
# Interactive upload
pinme upload
# Upload a specific file
pinme upload ./example.jpg
# Upload an entire directory
pinme upload ./my-website
```
### `list` / `ls`
Display upload history.
```bash
pinme list [options]
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
# Show the last 5 records
pinme ls -l 5
# Clear all history records
pinme list -c
```
### `help`
Display help information.
```bash
pinme help [command]
```
**Options:**
- `command`: The specific command to view help for (optional)
**Examples:**
```bash
# Display general help
pinme help
```
## Upload Limits
- Single file size limit: 100MB
- Total directory size limit: 500MB
## File Storage
Uploaded files are stored on the IPFS network and accessible through the Glitter Protocol's IPFS gateway. After a successful upload, you will receive:
1. IPFS hash value
2. Accessible URL link
### 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
## Contact Us
If you have questions or suggestions, please contact us through:
- GitHub Issues: [https://github.com/glitternetwork/pinme/issues](https://github.com/glitternetwork/pinme/issue)
- Email: [pinme@glitterprotocol.io](mailto:pinme@glitterprotocol.io)
---
Developed and maintained by the [Glitter Protocol](https://glitterprotocol.io/) team
+90
View File
@@ -0,0 +1,90 @@
import dotenv from 'dotenv';
dotenv.config();
import { Command } from "commander";
import chalk from "chalk";
import figlet from "figlet";
import { version } from '../package.json';
import upload from './upload';
import { displayUploadHistory, clearUploadHistory } from './utils/history';
// display the ASCII art logo
function showBanner(): void {
console.log(
chalk.cyan(
figlet.textSync("Pinme", { horizontalLayout: "full" })
)
);
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');
program
.command('upload')
.description("upload a file or directory to IPFS")
.action(() => upload());
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);
}
});
// add help command
program
.command('help')
.description("display help information")
.action(() => {
showBanner();
program.help();
});
// custom help output format
program.on('--help', () => {
console.log('');
console.log('Examples:');
console.log(' $ pinme upload');
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');
});
// parse the command line arguments
program.parse(process.argv);
// If no arguments provided, show banner and help
if (process.argv.length === 2) {
showBanner();
program.help();
}
+134
View File
@@ -0,0 +1,134 @@
import path from 'path';
import chalk from 'chalk';
import inquirer from 'inquirer';
import figlet from "figlet";
import upload from './utils/uploadToIpfs';
import fs from 'fs';
import CryptoJS from 'crypto-js';
// get from environment variables
const URL = process.env.IPFS_PREVIEW_URL;
const secretKey = process.env.SECRET_KEY;
// encrypt the hash
function encryptHash(hash: string, key: string|undefined): string {
try {
if (!key) {
throw new Error('Secret key not found');
}
const encrypted = CryptoJS.RC4.encrypt(hash, key).toString();
const urlSafe = encrypted
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
return urlSafe;
} catch (error: any) {
console.error(`Encryption error: ${error.message}`);
return hash;
}
}
// create a synchronous path check function
function checkPathSync(inputPath: string): string | null {
try {
// convert to absolute path
const absolutePath = path.resolve(inputPath);
// check if the path exists
if (fs.existsSync(absolutePath)) {
return absolutePath;
}
return null;
} catch (error: any) {
console.error(chalk.red(`error checking path: ${error.message}`));
return null;
}
}
interface UploadOptions {
[key: string]: any;
}
export default async (options?: UploadOptions): Promise<void> => {
try {
console.log(
figlet.textSync("PINME", {
font: "Shadow",
horizontalLayout: "default",
verticalLayout: "default",
width: 180,
whitespaceBreak: true,
})
);
// if the parameter is passed, upload directly, pinme upload /path/to/dir
const argPath = process.argv[3];
if (argPath && !argPath.startsWith('-')) {
// use the synchronous path check function
const absolutePath = checkPathSync(argPath);
if (!absolutePath) {
console.log(chalk.red(`path ${argPath} does not exist`));
return;
}
console.log(chalk.blue(`uploading ${absolutePath} to ipfs...`));
try {
const result = await upload(absolutePath);
if (result) {
const encryptedCID = encryptHash(result.contentHash, secretKey);
console.log(chalk.cyan(
figlet.textSync("Successful", { horizontalLayout: "full" })
))
console.log(chalk.cyan(`URL: ${URL}${encryptedCID}`));
} else {
console.log(chalk.red(`upload failed`));
}
} catch (error: any) {
console.error(chalk.red(`error uploading: ${error.message}`));
console.error(error.stack);
}
return;
}
const answer = await inquirer.prompt([
{
type: 'input',
name: 'path',
message: "path to upload: ",
},
]);
if (answer.path) {
// use the synchronous path check function
const absolutePath = checkPathSync(answer.path);
if (!absolutePath) {
console.log(chalk.red(`path ${answer.path} does not exist`));
return;
}
console.log(chalk.blue(`uploading ${absolutePath} to ipfs...`));
try {
const result = await upload(absolutePath);
if (result) {
const encryptedCID = encryptHash(result.contentHash, secretKey);
console.log(chalk.cyan(
figlet.textSync("Successful", { horizontalLayout: "full" })
))
console.log(chalk.cyan(`URL: ${URL}${encryptedCID}`));
} else {
console.log(chalk.red(`upload failed`));
}
} catch (error: any) {
console.error(chalk.red(`error uploading: ${error.message}`));
console.error(error.stack);
}
}
} catch (error: any) {
console.error(chalk.red(`error executing: ${error.message}`));
console.error(error.stack);
}
};
+54
View File
@@ -0,0 +1,54 @@
import fs from 'fs';
import path from 'path';
import Inquirer from "inquirer";
// check if the path exists and return the absolute path
function checkPath(inputPath: string): string | null {
try {
// convert to absolute path
const absolutePath = path.resolve(inputPath);
// check if the path exists
if (fs.existsSync(absolutePath)) {
return absolutePath;
}
return null;
} catch (error: any) {
console.error(`Error checking path: ${error.message}`);
return null;
}
}
export default async function(projectName: string): Promise<boolean> {
// get the current working directory
const cwd = process.cwd();
// get the project directory
const targetDirectory = path.join(cwd, projectName);
// check if the directory exists
if (fs.existsSync(targetDirectory)) {
let { isOverwrite } = await Inquirer.prompt([
{
name: "isOverwrite", // corresponding to the return value
type: "list", // list type
message: "Target directory exists, Please choose an action",
choices: [
{ name: "Overwrite", value: true },
{ name: "Cancel", value: false },
],
},
]);
// choose Cancel
if (!isOverwrite) {
console.log("\n Canceled \n");
return false;
} else {
// choose Overwrite, delete the existing directory
console.log("Removing folder...");
await fs.promises.rm(targetDirectory, { recursive: true, force: true });
console.log("Folder removed!");
return true
}
} else {
return true;
}
};
+18
View File
@@ -0,0 +1,18 @@
import CryptoJS from "crypto-js";
export const decryptHash = (encryptedHash: string | undefined, key: string) => {
try {
if (!encryptedHash) {
return null;
}
let base64 = encryptedHash.replace(/-/g, "+").replace(/_/g, "/");
while (base64.length % 4) {
base64 += "=";
}
const decrypted = CryptoJS.RC4.decrypt(base64, key);
return decrypted.toString(CryptoJS.enc.Utf8);
} catch (error: any) {
console.error(`Decryption error: ${error.message}`);
return encryptedHash;
}
};
+21
View File
@@ -0,0 +1,21 @@
import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import { v4 as uuidv4 } from 'uuid';
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;
}
+118
View File
@@ -0,0 +1,118 @@
import chalk from 'chalk';
import figlet from 'figlet';
// show ASCII art banner
function showBanner(): void {
console.log(
chalk.cyan(
figlet.textSync("Pinme CLI", { horizontalLayout: "full" })
)
);
console.log(chalk.cyan("A command-line tool for uploading files to IPFS\n"));
}
// general help
function showGeneralHelp(): void {
showBanner();
console.log("USAGE:");
console.log(" pinme [command] [options]\n");
console.log("COMMANDS:");
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(" help [command] Show help for a specific command\n");
console.log("OPTIONS:");
console.log(" -v, --version Output the current version");
console.log(" -h, --help Display help for command\n");
console.log("For more information on a specific command, try:");
console.log(" pinme help [command]");
}
// upload command help
function showUploadHelp(): void {
console.log("COMMAND:");
console.log(" upload - Upload a file or directory to IPFS\n");
console.log("USAGE:");
console.log(" pinme upload [path]\n");
console.log("DESCRIPTION:");
console.log(" This command uploads files or directories to IPFS.");
console.log(" If no path is provided, it will start in interactive mode.\n");
console.log("EXAMPLES:");
console.log(" pinme upload");
console.log(" pinme upload ./my-website\n");
console.log("LIMITATIONS:");
console.log(" - Maximum file size: 10MB");
console.log(" - Maximum directory size: 500MB");
}
// list command help
function showListHelp(): void {
console.log("COMMAND:");
console.log(" list - Show upload history\n");
console.log("USAGE:");
console.log(" pinme list [options]\n");
console.log("OPTIONS:");
console.log(" -l, --limit <number> Limit the number of records to show");
console.log(" -c, --clear Clear all upload history\n");
console.log("EXAMPLES:");
console.log(" pinme list");
console.log(" pinme list -l 5");
console.log(" pinme list -c");
}
// ls command help (can reuse the list command help)
function showLsHelp(): void {
console.log("COMMAND:");
console.log(" ls - Alias for 'list' command\n");
console.log("USAGE:");
console.log(" pinme ls [options]\n");
console.log("OPTIONS:");
console.log(" -l, --limit <number> Limit the number of records to show");
console.log(" -c, --clear Clear all upload history\n");
console.log("EXAMPLES:");
console.log(" pinme ls");
console.log(" pinme ls -l 5");
console.log(" pinme ls -c");
}
// show the help for the command
function showHelp(command?: string): void {
if (!command) {
showGeneralHelp();
return;
}
switch (command) {
case 'upload':
showUploadHelp();
break;
case 'list':
showListHelp();
break;
case 'ls':
showLsHelp();
break;
default:
console.log(`Unknown command: ${command}`);
showGeneralHelp();
}
}
export {
showHelp,
showBanner
};
+149
View File
@@ -0,0 +1,149 @@
import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import dayjs from 'dayjs';
import chalk from 'chalk';
import { formatSize } from './uploadLimits';
// history file path
const HISTORY_DIR = path.join(os.homedir(), '.pinme');
const HISTORY_FILE = path.join(HISTORY_DIR, 'upload-history.json');
interface UploadRecord {
timestamp: number;
date: string;
path: string;
filename: string;
contentHash: string;
previewHash: string | null;
size: number;
fileCount: number;
type: 'directory' | 'file';
}
interface UploadHistory {
uploads: UploadRecord[];
}
interface UploadData {
path: string;
filename?: string;
contentHash: string;
previewHash: string | null;
size: number;
fileCount?: number;
isDirectory?: boolean;
}
// ensure the history directory exists
const ensureHistoryDir = (): void => {
if (!fs.existsSync(HISTORY_DIR)) {
fs.mkdirSync(HISTORY_DIR, { recursive: true });
}
if (!fs.existsSync(HISTORY_FILE)) {
fs.writeJsonSync(HISTORY_FILE, { uploads: [] });
}
};
// save the upload history
const saveUploadHistory = (uploadData: UploadData): boolean => {
try {
ensureHistoryDir();
const history = fs.readJsonSync(HISTORY_FILE) as UploadHistory;
// add new upload record
const newRecord: UploadRecord = {
timestamp: Date.now(),
date: dayjs().format('YYYY-MM-DD HH:mm:ss'),
path: uploadData.path,
filename: uploadData.filename || path.basename(uploadData.path),
contentHash: uploadData.contentHash,
previewHash: uploadData.previewHash,
size: uploadData.size,
fileCount: uploadData.fileCount || 1,
type: uploadData.isDirectory ? 'directory' : 'file'
};
history.uploads.unshift(newRecord); // add to the beginning
// write to file
fs.writeJsonSync(HISTORY_FILE, history, { spaces: 2 });
return true;
} catch (error: any) {
console.error(chalk.red(`Error saving upload history: ${error.message}`));
return false;
}
};
// get the upload history
const getUploadHistory = (limit: number = 10): UploadRecord[] => {
try {
ensureHistoryDir();
const history = fs.readJsonSync(HISTORY_FILE) as UploadHistory;
return history.uploads.slice(0, limit);
} catch (error: any) {
console.error(chalk.red(`Error reading upload history: ${error.message}`));
return [];
}
};
// display the upload history
const displayUploadHistory = (limit: number = 10): void => {
const history = getUploadHistory(limit);
if (history.length === 0) {
console.log(chalk.yellow('No upload history found.'));
return;
}
console.log(chalk.bold('\n📜 Upload History:'));
console.log(chalk.dim('─'.repeat(80)));
history.forEach((record, index) => {
console.log(chalk.bold(`#${index + 1} - ${record.date}`));
console.log(chalk.cyan(`Name: ${record.filename}`));
console.log(chalk.cyan(`Path: ${record.path}`));
console.log(chalk.cyan(`Type: ${record.type}`));
console.log(chalk.cyan(`Size: ${formatSize(record.size)}`));
if (record.type === 'directory') {
console.log(chalk.cyan(`Files: ${record.fileCount}`));
}
console.log(chalk.cyan(`Content Hash: ${record.contentHash}`));
if (record.previewHash) {
console.log(chalk.cyan(`Preview Hash: ${record.previewHash}`));
console.log(chalk.cyan(`URL: https://ipfs.glitterprotocol.dev/ipfs/${record.previewHash}/#/?from=local`));
} else {
console.log(chalk.cyan(`URL: https://ipfs.glitterprotocol.dev/ipfs/${record.contentHash}`));
}
console.log(chalk.dim('─'.repeat(80)));
});
// display the statistics
const totalSize = history.reduce((sum, record) => sum + record.size, 0);
const totalFiles = history.reduce((sum, record) => sum + record.fileCount, 0);
console.log(chalk.bold(`Total Uploads: ${history.length}`));
console.log(chalk.bold(`Total Files: ${totalFiles}`));
console.log(chalk.bold(`Total Size: ${formatSize(totalSize)}`));
};
// clear the upload history
const clearUploadHistory = (): boolean => {
try {
ensureHistoryDir();
fs.writeJsonSync(HISTORY_FILE, { uploads: [] });
console.log(chalk.green('Upload history cleared successfully.'));
return true;
} catch (error: any) {
console.error(chalk.red(`Error clearing upload history: ${error.message}`));
return false;
}
};
export {
saveUploadHistory,
getUploadHistory,
displayUploadHistory,
clearUploadHistory
};
+69
View File
@@ -0,0 +1,69 @@
import fs from 'fs';
import path from 'path';
const FILE_SIZE_LIMIT = parseInt(process.env.FILE_SIZE_LIMIT || '100', 10) * 1024 * 1024; // MB to bytes
const DIRECTORY_SIZE_LIMIT = parseInt(process.env.DIRECTORY_SIZE_LIMIT || '500', 10) * 1024 * 1024; // MB to bytes
interface FileSizeCheck {
size: number;
exceeds: boolean;
limit: number;
}
interface DirectorySizeCheck {
size: number;
limit: number;
exceeds: boolean;
}
function checkFileSizeLimit(filePath: string): FileSizeCheck {
const stats = fs.statSync(filePath);
return {
size: stats.size,
limit: FILE_SIZE_LIMIT,
exceeds: stats.size > FILE_SIZE_LIMIT
};
}
function checkDirectorySizeLimit(directoryPath: string): DirectorySizeCheck {
const totalSize = calculateDirectorySize(directoryPath);
return {
size: totalSize,
limit: DIRECTORY_SIZE_LIMIT,
exceeds: totalSize > DIRECTORY_SIZE_LIMIT
};
}
function calculateDirectorySize(directoryPath: string): number {
let totalSize = 0;
const files = fs.readdirSync(directoryPath);
for (const file of files) {
const filePath = path.join(directoryPath, file);
const stats = fs.statSync(filePath);
if (stats.isFile()) {
totalSize += stats.size;
} else if (stats.isDirectory()) {
totalSize += calculateDirectorySize(filePath);
}
}
return totalSize;
}
function formatSize(bytes: number): string {
if (bytes < 1024) return bytes + ' bytes';
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
else if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
else return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
export {
checkFileSizeLimit,
checkDirectorySizeLimit,
calculateDirectorySize,
formatSize,
FILE_SIZE_LIMIT,
DIRECTORY_SIZE_LIMIT
};
+219
View File
@@ -0,0 +1,219 @@
import axios from 'axios';
import fs from 'fs-extra';
import path from 'path';
import FormData from 'form-data';
import ora from 'ora';
import chalk from 'chalk';
import {
checkFileSizeLimit,
checkDirectorySizeLimit,
formatSize,
} from './uploadLimits';
import { saveUploadHistory } from './history';
import { getDeviceId } from './getDeviceId';
const ipfsApiUrl = process.env.IPFS_API_URL || 'https://ipfs.glitterprotocol.dev/api/v2';
interface FileInfo {
name: string;
path: string;
}
interface IpfsResponse {
data: {
data: Array<{
Name: string;
Hash: string;
Size: string;
}>;
};
}
// dist is the directory name, dirPath is all paths before dist
let dirPath: string | null = null;
function loadFilesToArrRecursively(directoryPath: string, dist: string): FileInfo[] {
const filesArr: FileInfo[] = [];
const sep = path.sep;
dirPath ??= directoryPath.replace(dist, '');
// check if it is a directory
if (fs.statSync(directoryPath).isDirectory()) {
const files = fs.readdirSync(directoryPath);
files.forEach((file) => {
const filePath = path.join(directoryPath, file);
if (fs.statSync(filePath).isFile()) {
// check the file size
const sizeCheck = checkFileSizeLimit(filePath);
if (sizeCheck.exceeds) {
throw new Error(`File ${file} exceeds size limit of ${formatSize(sizeCheck.limit)} (size: ${formatSize(sizeCheck.size)})`);
}
const filePathWithNoEndSep = filePath.replace(dirPath!, '');
const filePathEncodeSep = filePathWithNoEndSep.replaceAll(sep, '%2F');
filesArr.push({
name: filePathEncodeSep,
path: filePath,
});
} else if (fs.statSync(filePath).isDirectory()) {
const recursiveFiles = loadFilesToArrRecursively(filePath, dist);
filesArr.push(...recursiveFiles);
}
});
} else {
console.error('Error: path must be a directory');
}
return filesArr;
}
function countFilesInDirectory(directoryPath: string): number {
let count = 0;
const files = fs.readdirSync(directoryPath);
for (const file of files) {
const filePath = path.join(directoryPath, file);
const stats = fs.statSync(filePath);
if (stats.isFile()) {
count++;
} else if (stats.isDirectory()) {
count += countFilesInDirectory(filePath);
}
}
return count;
}
// upload directory to ipfs
async function uploadDirectory(directoryPath: string, deviceId: string): Promise<string | null> {
// check the size of all files in the directory
const sizeCheck = checkDirectorySizeLimit(directoryPath);
// check the size limit of single file
if (sizeCheck.exceeds) {
throw new Error(`Directory ${directoryPath} exceeds size limit of ${formatSize(sizeCheck.limit)} (size: ${formatSize(sizeCheck.size)})`);
}
const formData = new FormData();
// redundant check for directoryPath, ensure directoryPath ends with no separator
if (directoryPath.endsWith(path.sep)) directoryPath = directoryPath.slice(0, -1);
// get the last layer directory, as the ipfs directory name
const dist = directoryPath.split(path.sep).pop() || '';
// recursively get all files
const files = loadFilesToArrRecursively(directoryPath, dist);
files.forEach((file) => {
formData.append('file', fs.createReadStream(file.path), {
filename: file.name,
});
});
formData.append('uid', deviceId);
const spinner = ora(`Uploading ${directoryPath} to glitter ipfs...`).start();
const response = await axios.post<IpfsResponse['data']>(`${ipfsApiUrl}/add`, formData, {
headers: {
...formData.getHeaders(),
},
});
const resData = response.data.data;
// check if the returned data is an array and contains at least one element
if (Array.isArray(resData) && resData.length > 0) {
spinner.succeed();
// find the object with Name as an empty string, get the directory hash
const directoryItem = resData.find((item) => item.Name === dist);
if (directoryItem) {
const fileStats = fs.statSync(directoryPath);
const fileCount = countFilesInDirectory(directoryPath);
const uploadData = {
path: directoryPath,
filename: path.basename(directoryPath),
contentHash: directoryItem.Hash,
previewHash: null,
size: sizeCheck.size,
fileCount: fileCount,
isDirectory: true
};
saveUploadHistory(uploadData);
return directoryItem.Hash;
}
spinner.fail();
console.log(chalk.red(`Directory hash not found in response`));
} else {
spinner.fail();
console.log(chalk.red(`Invalid response format from IPFS`));
}
return null;
}
// upload file to ipfs
async function uploadFile(filePath: string, deviceId: string): Promise<string | null> {
const sizeCheck = checkFileSizeLimit(filePath);
if (sizeCheck.exceeds) {
throw new Error(`File ${filePath} exceeds size limit of ${formatSize(sizeCheck.limit)} (size: ${formatSize(sizeCheck.size)})`);
}
const formData = new FormData();
formData.append('file', fs.createReadStream(filePath), {
filename: filePath.split(path.sep).pop() || '',
});
formData.append('uid', deviceId);
const spinner = ora(`Uploading ${filePath} to glitter ipfs...`).start();
const response = await axios.post<IpfsResponse['data']>(`${ipfsApiUrl}/add`, formData, {
headers: {
...formData.getHeaders(),
},
});
const resData = response.data.data;
// check if the returned data is an array and contains at least one element
if (Array.isArray(resData) && resData.length > 0) {
spinner.succeed();
// find the object with Name as an empty string, get the file hash
const fileItem = resData.find((item) => item.Name === filePath.split(path.sep).pop() || '');
if (fileItem) {
const uploadData = {
path: filePath,
filename: filePath.split(path.sep).pop() || '',
contentHash: fileItem.Hash,
previewHash: null,
size: sizeCheck.size,
fileCount: 1,
isDirectory: false
};
saveUploadHistory(uploadData);
return fileItem.Hash;
}
spinner.fail();
console.log(chalk.red(`File hash not found in response`));
} else {
spinner.fail();
console.log(chalk.red(`Invalid response format from IPFS`));
}
return null;
}
export default async function(filePath: string): Promise<{contentHash: string, previewHash?: string | null}> {
// check if the file is a directory
const deviceId = getDeviceId();
if (!deviceId) {
throw new Error('Device ID not found');
}
if (fs.statSync(filePath).isDirectory()) {
return {
contentHash: await uploadDirectory(filePath, deviceId) || '',
previewHash: null
};
} else {
return {
contentHash: await uploadFile(filePath, deviceId) || '',
previewHash: null
};
}
}
+23
View File
@@ -0,0 +1,23 @@
require('dotenv').config();
const esbuild = require('esbuild');
const define = {};
for (const key in process.env) {
define[`process.env.${key}`] = JSON.stringify(process.env[key]);
}
define['process.env.IPFS_PREVIEW_URL'] = JSON.stringify(process.env.IPFS_PREVIEW_URL);
define['process.env.SECRET_KEY'] = JSON.stringify(process.env.SECRET_KEY);
esbuild.build({
entryPoints: ['bin/index.ts'],
outfile: 'dist/index.js',
bundle: true,
platform: 'node',
target: 'node14',
format: 'cjs',
external: Object.keys(require('./package.json').dependencies || {}),
banner: { js: '#!/usr/bin/env node' },
logLevel: 'info',
define,
}).catch(() => process.exit(1));
Vendored Executable
BIN
View File
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
sh build.sh
blog run serve & pinme upload ./dist
ghost
+1
View File
@@ -0,0 +1 @@
docsify
+63
View File
@@ -0,0 +1,63 @@
{
"name": "pinme",
"version": "1.0.1",
"publishConfig": {
"access": "public"
},
"description": "Deploy Your Frontend In a Single Command",
"main": "dist/index.js",
"scripts": {
"build": "node build.js",
"dev": "NODE_ENV=development node build.js",
"prepublishOnly": "npm run build"
},
"bin": {
"pinme": "./dist/index.js"
},
"files": [
"dist"
],
"keywords": [
"ipfs",
"cli",
"deploy",
"frontend"
],
"author": "Glitter Protocol",
"license": "MIT",
"dependencies": {
"axios": "^1.3.2",
"base-x": "^5.0.1",
"bip39": "^3.1.0",
"chalk": "^2.4.2",
"commander": "^11.1.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.7",
"ethers": "5.7.2",
"figlet": "^1.7.0",
"form-data": "^4.0.0",
"fs-extra": "^11.2.0",
"inquirer": "^8.2.5",
"ora": "^3.2.0",
"uuid": "^9.0.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^14.1.0",
"dotenv": "^16.5.0",
"esbuild": "^0.25.2",
"eslint": "^8.33.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.8.3",
"rollup": "^2.79.1",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-terser": "^7.0.2"
},
"engines": {
"node": ">= 14.18.0"
}
}
+3330
View File
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const json = require('@rollup/plugin-json');
const typescript = require('@rollup/plugin-typescript');
const { terser } = require('rollup-plugin-terser');
const copy = require('rollup-plugin-copy');
const path = require('path');
const fs = require('fs');
// 确保dist目录存在
const distDir = path.resolve(__dirname, 'dist');
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
module.exports = {
input: 'bin/index.ts',
output: {
file: 'dist/index.js',
format: 'cjs',
banner: '#!/usr/bin/env node',
sourcemap: false,
},
// 将所有npm依赖项设为外部依赖,不打包进最终文件
external: [
...Object.keys(require('./package.json').dependencies || {}),
...require('module').builtinModules,
],
plugins: [
typescript({
tsconfig: './tsconfig.json',
sourceMap: false
}),
// 解析第三方模块
nodeResolve({
preferBuiltins: true,
}),
// 转换CommonJS模块为ES模块
commonjs(),
// 支持导入JSON文件
json(),
// 压缩代码
terser({
format: {
comments: false,
},
compress: {
drop_console: false,
drop_debugger: true,
},
}),
// 自定义插件:确保生成的文件有执行权限
{
name: 'make-executable',
writeBundle() {
const outputFile = path.resolve(__dirname, 'dist/index.js');
try {
fs.chmodSync(outputFile, '755');
console.log('Added executable permissions to output file');
} catch (error) {
console.error('Failed to add executable permissions:', error);
}
}
}
],
onwarn(warning, warn) {
// 忽略某些警告
if (warning.code === 'CIRCULAR_DEPENDENCY') return;
if (warning.code === 'UNRESOLVED_IMPORT' && warning.source.startsWith('node:')) return;
warn(warning);
}
};
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext"],
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"types": ["node"],
"outDir": "dist",
"declaration": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["bin/**/*"],
"exclude": ["node_modules", "dist"],
}
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}