mirror of
https://github.com/besoeasy/file-drop.git
synced 2025-12-30 15:39:49 -06:00
Initial commit
This commit is contained in:
139
.github/workflows/docker.yml
vendored
Normal file
139
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
outputs:
|
||||
version_changed: ${{ steps.version-check.outputs.version_changed }}
|
||||
version: ${{ steps.version-check.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if package.json version changed
|
||||
id: version-check
|
||||
run: |
|
||||
if [ $(git rev-list --count HEAD) -eq 1 ]; then
|
||||
echo "version_changed=true" >> $GITHUB_OUTPUT
|
||||
echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
current_version=$(jq -r .version package.json)
|
||||
previous_version=$(git show HEAD^:package.json | jq -r .version)
|
||||
if [ "$previous_version" = "null" ] || [ "$current_version" != "$previous_version" ]; then
|
||||
echo "version_changed=true" >> $GITHUB_OUTPUT
|
||||
echo "version=$current_version" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "version_changed=false" >> $GITHUB_OUTPUT
|
||||
echo "version=$current_version" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request' && steps.version-check.outputs.version_changed == 'true'
|
||||
uses: sigstore/cosign-installer@v3.5.0
|
||||
with:
|
||||
Irlanda-release: 'v2.2.4'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.version-check.outputs.version_changed == 'true'
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request' && steps.version-check.outputs.version_changed == 'true'
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
if: steps.version-check.outputs.version_changed == 'true'
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.0.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=main
|
||||
type=raw,value=${{ steps.version-check.outputs.version }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
if: steps.version-check.outputs.version_changed == 'true'
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Sign the published Docker image
|
||||
if: github.event_name != 'pull_request' && steps.version-check.outputs.version_changed == 'true'
|
||||
env:
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
|
||||
|
||||
testing:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: ${{ needs.build.outputs.version_changed == 'false' }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.0.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=test
|
||||
|
||||
- name: Build and push Docker image (test)
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
temp_uploads/
|
||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
||||
FROM docker.io/node:lts-slim
|
||||
|
||||
ENV STORAGE_MAX=200GB
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -fsSL "https://dist.ipfs.tech/kubo/v0.38.2/kubo_v0.38.2_linux-$(dpkg --print-architecture).tar.gz" | \
|
||||
tar -xz -C /tmp && \
|
||||
mv /tmp/kubo/ipfs /usr/local/bin/ipfs && \
|
||||
rm -rf /tmp/kubo
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
RUN npm i
|
||||
|
||||
COPY . .
|
||||
|
||||
# Create uploads directory and set ownership
|
||||
RUN mkdir -p /app/temp_uploads && chown -R node:node /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER node
|
||||
|
||||
EXPOSE 3232 4001/tcp 4001/udp
|
||||
|
||||
CMD ["sh", "-c", "\
|
||||
if [ ! -d \"$HOME/.ipfs\" ]; then ipfs init; fi && \
|
||||
ipfs config Datastore.StorageMax ${STORAGE_MAX} && \
|
||||
ipfs daemon --enable-gc & \
|
||||
until curl -s http://127.0.0.1:5001/api/v0/id > /dev/null; do \
|
||||
echo 'Waiting for IPFS daemon...'; sleep 3; \
|
||||
done && \
|
||||
exec node app.js"]
|
||||
68
README.md
Normal file
68
README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# File Drop
|
||||
|
||||
_A open-source solution for sharing images, videos, and any other files._
|
||||
|
||||
File Drop is a lightweight, peer-to-peer (P2P) application that allows users to share files directly between devices without relying on centralized servers. Built with privacy and simplicity in mind, it leverages Docker for easy deployment and P2P protocols for efficient, secure file transfers. With File Drop, you can upload anything without the fear of being censored since your data is relayed through your own computer. Even if your node loses its internet connection, your files persist across the entire IPFS network, ensuring availability.
|
||||
|
||||

|
||||
|
||||
File Drop is not built for permanent storage. Think of it as a way to share files temporarily on networks like Nostr, forums, and other apps. Since IPFS itself can be memory-intensive, we’ve designed File Drop to be lightweight. If you need permanent storage, you can edit 2-3 lines in the code and set pinning to `true`, but that’s not our aim. File Drop supports any file type—be it images, videos, text files, or anything else—with a current limit of 250 MB per file, as the IPFS network isn’t mature enough to handle larger files reliably. However, with a powerful enough computer, there are practically no limits.
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command to start File Drop:
|
||||
|
||||
```bash
|
||||
docker run -d --restart unless-stopped \
|
||||
-p 3232:3232 \
|
||||
-p 4001:4001/tcp \
|
||||
-p 4001:4001/udp \
|
||||
--name file-drop \
|
||||
-e STORAGE_MAX=50GB \
|
||||
ghcr.io/besoeasy/file-drop:main
|
||||
```
|
||||
|
||||
## Portainer Stack
|
||||
|
||||
For easy deployment with Portainer, use this stack configuration:
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
file-drop:
|
||||
image: ghcr.io/besoeasy/file-drop:main
|
||||
container_name: file-drop
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3232:3232"
|
||||
- "4001:4001/tcp"
|
||||
- "4001:4001/udp"
|
||||
environment:
|
||||
- STORAGE_MAX=50GB
|
||||
```
|
||||
|
||||
**Steps to deploy with Portainer:**
|
||||
|
||||
1. Open Portainer and navigate to **Stacks**
|
||||
2. Click **Add stack** and give it a name (e.g., "file-drop")
|
||||
3. Paste the above YAML configuration in the editor
|
||||
4. Adjust environment variables as needed
|
||||
5. Click **Deploy the stack**
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- **STORAGE_MAX**: Sets the maximum storage limit for the IPFS repository.
|
||||
- Default: `200GB`
|
||||
- Example: `-e STORAGE_MAX=50GB` limits IPFS storage to 50 GB
|
||||
- Formats: Supports standard units like `MB`, `GB`, `TB`
|
||||
- Note: This controls how much disk space IPFS can use for storing files and metadata
|
||||
|
||||
## Usage
|
||||
|
||||
Access the application via `http://localhost:3232` in your browser (or your machine’s IP if remote).
|
||||
272
app.js
Normal file
272
app.js
Normal file
@@ -0,0 +1,272 @@
|
||||
const express = require("express");
|
||||
const multer = require("multer");
|
||||
const axios = require("axios");
|
||||
const FormData = require("form-data");
|
||||
const path = require("path");
|
||||
const cors = require("cors");
|
||||
const compression = require("compression");
|
||||
const fs = require("fs");
|
||||
const { promisify } = require("util");
|
||||
const unlinkAsync = promisify(fs.unlink);
|
||||
|
||||
// Constants
|
||||
const IPFS_API = "http://127.0.0.1:5001";
|
||||
const PORT = 3232;
|
||||
const STORAGE_MAX = process.env.STORAGE_MAX || "200GB";
|
||||
const HOST = "0.0.0.0";
|
||||
const UPLOAD_TEMP_DIR = path.join(__dirname, "temp_uploads");
|
||||
|
||||
// Ensure temp directory exists
|
||||
if (!fs.existsSync(UPLOAD_TEMP_DIR)) {
|
||||
fs.mkdirSync(UPLOAD_TEMP_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Initialize Express app
|
||||
const app = express();
|
||||
|
||||
// Middleware setup
|
||||
app.use(compression()); // Enable gzip/deflate compression
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cors());
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
// Configure multer for file uploads with disk storage for streaming
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, UPLOAD_TEMP_DIR);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// Generate unique filename with timestamp
|
||||
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
||||
cb(null, uniqueSuffix + "-" + file.originalname);
|
||||
},
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter: (req, file, cb) => {
|
||||
cb(null, true);
|
||||
},
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
console.error("Unexpected error:", err.stack);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
};
|
||||
|
||||
// Enhanced status endpoint
|
||||
app.get("/status", async (req, res) => {
|
||||
try {
|
||||
// Fetch multiple IPFS stats concurrently
|
||||
const [bwResponse, repoResponse, idResponse] = await Promise.all([
|
||||
axios.post(`${IPFS_API}/api/v0/stats/bw?interval=5m`, { timeout: 5000 }),
|
||||
axios.post(`${IPFS_API}/api/v0/repo/stat`, { timeout: 5000 }),
|
||||
axios.post(`${IPFS_API}/api/v0/id`, { timeout: 5000 }),
|
||||
]);
|
||||
|
||||
// Format bandwidth data
|
||||
const bandwidth = {
|
||||
totalIn: bwResponse.data.TotalIn,
|
||||
totalOut: bwResponse.data.TotalOut,
|
||||
rateIn: bwResponse.data.RateIn,
|
||||
rateOut: bwResponse.data.RateOut,
|
||||
interval: "1h",
|
||||
};
|
||||
|
||||
// Format repository stats
|
||||
const repo = {
|
||||
size: repoResponse.data.RepoSize,
|
||||
storageMax: repoResponse.data.StorageMax,
|
||||
numObjects: repoResponse.data.NumObjects,
|
||||
path: repoResponse.data.RepoPath,
|
||||
version: repoResponse.data.Version,
|
||||
};
|
||||
|
||||
// Get GC configuration info
|
||||
let gcInfo = {
|
||||
enabled: true,
|
||||
period: "200h",
|
||||
lastRun: "Unknown",
|
||||
};
|
||||
|
||||
try {
|
||||
const configResponse = await axios.post(`${IPFS_API}/api/v0/config/show`, { timeout: 3000 });
|
||||
if (configResponse.data && configResponse.data.Datastore) {
|
||||
gcInfo.period = configResponse.data.Datastore.GCPeriod || "200h";
|
||||
}
|
||||
} catch (configErr) {
|
||||
console.log("Could not fetch GC config:", configErr.message);
|
||||
}
|
||||
|
||||
// Node identity info
|
||||
const nodeInfo = {
|
||||
id: idResponse.data.ID,
|
||||
publicKey: idResponse.data.PublicKey,
|
||||
addresses: idResponse.data.Addresses,
|
||||
agentVersion: idResponse.data.AgentVersion,
|
||||
protocolVersion: idResponse.data.ProtocolVersion,
|
||||
};
|
||||
|
||||
const peersResponse = await axios.post(`${IPFS_API}/api/v0/swarm/peers`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const connectedPeers = {
|
||||
count: peersResponse.data.Peers.length,
|
||||
list: peersResponse.data.Peers,
|
||||
};
|
||||
|
||||
// Get app version from package.json
|
||||
const { version: appVersion } = require("./package.json");
|
||||
|
||||
// Format file size limit in human readable form
|
||||
const formatBytes = (bytes) => {
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + " " + sizes[i];
|
||||
};
|
||||
|
||||
res.json({
|
||||
status: "success",
|
||||
timestamp: new Date().toISOString(),
|
||||
bandwidth,
|
||||
repository: repo,
|
||||
node: nodeInfo,
|
||||
peers: connectedPeers,
|
||||
garbageCollection: gcInfo,
|
||||
storageLimit: {
|
||||
configured: STORAGE_MAX,
|
||||
current: formatBytes(repo.storageMax),
|
||||
},
|
||||
appVersion,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Status check error:", {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
res.status(503).json({
|
||||
error: "Failed to retrieve IPFS status",
|
||||
details: err.message,
|
||||
status: "failed",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Upload endpoint
|
||||
app.post("/upload", upload.single("file"), async (req, res) => {
|
||||
let filePath = null;
|
||||
|
||||
try {
|
||||
// Validate file presence
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
error: "No file uploaded",
|
||||
status: "error",
|
||||
message: "No file uploaded",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
filePath = req.file.path;
|
||||
|
||||
// Prepare file for IPFS using stream instead of buffer
|
||||
const formData = new FormData();
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
|
||||
formData.append("file", fileStream, {
|
||||
filename: req.file.originalname,
|
||||
contentType: req.file.mimetype,
|
||||
knownLength: req.file.size,
|
||||
});
|
||||
|
||||
// Upload to IPFS
|
||||
const uploadStart = Date.now();
|
||||
const response = await axios.post(`${IPFS_API}/api/v0/add`, formData, {
|
||||
headers: { ...formData.getHeaders() },
|
||||
timeout: 60000, // Increased to 60s for large files
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
});
|
||||
|
||||
// Clean up temp file after successful upload
|
||||
await unlinkAsync(filePath).catch((err) => console.warn("Failed to delete temp file:", err.message));
|
||||
|
||||
// Detailed logging
|
||||
const uploadDetails = {
|
||||
name: req.file.originalname,
|
||||
size_bytes: req.file.size,
|
||||
mime_type: req.file.mimetype,
|
||||
cid: response.data.Hash,
|
||||
upload_duration_ms: Date.now() - uploadStart,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
console.log("File uploaded successfully:", uploadDetails);
|
||||
|
||||
// NIP-96 compatible response with your existing fields
|
||||
res.json({
|
||||
// NIP-96 required fields
|
||||
status: "success",
|
||||
message: "Upload successful.",
|
||||
nip94_event: {
|
||||
tags: [
|
||||
["url", `https://dweb.link/ipfs/${response.data.Hash}`],
|
||||
["m", req.file.mimetype || "application/octet-stream"],
|
||||
["ox", response.data.Hash], // Original file hash (required)
|
||||
["x", response.data.Hash], // Transformed file hash (same as original since no transformation)
|
||||
["size", req.file.size.toString()],
|
||||
],
|
||||
content: "", // Required field, can be empty
|
||||
},
|
||||
|
||||
// Your existing fields for compatibility
|
||||
cid: response.data.Hash,
|
||||
filename: req.file.originalname,
|
||||
size: req.file.size,
|
||||
details: uploadDetails,
|
||||
});
|
||||
} catch (err) {
|
||||
// Clean up temp file on error
|
||||
if (filePath) {
|
||||
await unlinkAsync(filePath).catch((cleanupErr) => console.warn("Failed to delete temp file on error:", cleanupErr.message));
|
||||
}
|
||||
|
||||
if (err instanceof multer.MulterError) {
|
||||
return res.status(400).json({
|
||||
error: err.message,
|
||||
status: "error",
|
||||
message: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
console.error("IPFS upload error:", {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
error: "Failed to upload to IPFS",
|
||||
details: err.message,
|
||||
status: "error",
|
||||
message: "Failed to upload to IPFS",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Apply error handler
|
||||
app.use(errorHandler);
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`Server running on http://${HOST}:${PORT}`);
|
||||
console.log(`IPFS API endpoint: ${IPFS_API}`);
|
||||
});
|
||||
1179
package-lock.json
generated
Normal file
1179
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "file-drop",
|
||||
"version": "1.1.1",
|
||||
"description": "file drop",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"test": "podman run --name ipfs-host --rm --network host docker.io/ipfs/kubo:latest",
|
||||
"start": "node app.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/besoeasy/file-drop.git"
|
||||
},
|
||||
"author": "besoeasy",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/besoeasy/file-drop/issues"
|
||||
},
|
||||
"homepage": "https://github.com/besoeasy/file-drop#readme",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2",
|
||||
"form-data": "^4.0.2",
|
||||
"multer": "^1.4.5-lts.2"
|
||||
}
|
||||
}
|
||||
709
public/index.html
Normal file
709
public/index.html
Normal file
@@ -0,0 +1,709 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>File Drop</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
/* Import modern font */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
|
||||
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
/* Enhanced animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-in-left {
|
||||
animation: slideInLeft 0.5s ease-out;
|
||||
}
|
||||
|
||||
.animate-pulse-gentle {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
background: linear-gradient(90deg, #000 25%, #333 37%, #000 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Enhanced glassmorphism */
|
||||
.glassmorphism {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.glassmorphism-dark {
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Tab styles */
|
||||
.tab-button {
|
||||
transition: all 0.3s ease;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
border-bottom-color: #000;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.tab-button:hover:not(.active) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Enhanced hover effects */
|
||||
.hover-lift {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Status indicators */
|
||||
.status-indicator {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-indicator::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-gray-100 flex items-center justify-center p-4">
|
||||
<div id="app" class="w-full max-w-6xl glassmorphism rounded-2xl shadow-xl overflow-hidden hover-lift" tabindex="0"
|
||||
@paste="handlePaste" @keydown.prevent>
|
||||
<!-- Header -->
|
||||
<header class="bg-black p-6 text-white text-center">
|
||||
<div class="flex items-center justify-center gap-3 mb-2">
|
||||
<i class="fas fa-cloud-upload-alt text-2xl"></i>
|
||||
<h1 class="text-3xl font-bold">File Drop</h1>
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm"> Anonymous File Sharing</p>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<!-- Sidebar: Status Panel -->
|
||||
<aside class="w-full md:w-80 bg-gray-50 p-6 border-r border-gray-200">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<div class="status-indicator">
|
||||
<i class="fas fa-server text-xl text-black"></i>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-black">Node Status</h2>
|
||||
</div>
|
||||
<div class="space-y-4 text-sm">
|
||||
<div class="bg-white rounded-lg p-3 shadow-sm border border-gray-200">
|
||||
<p class="text-gray-500 font-medium text-xs uppercase">Node ID</p>
|
||||
<p class="text-black truncate font-mono text-sm">{{ status.nodeId }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3 shadow-sm border border-gray-200">
|
||||
<p class="text-gray-500 font-medium text-xs uppercase">Connected Peers</p>
|
||||
<p class="text-black font-semibold">{{ status.peerscount }} Nodes</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3 shadow-sm border border-gray-200">
|
||||
<p class="text-gray-500 font-medium text-xs uppercase">Bandwidth (1h)</p>
|
||||
<p class="text-black font-mono text-sm">{{ status.bandwidth }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3 shadow-sm border border-gray-200">
|
||||
<p class="text-gray-500 font-medium text-xs uppercase">Repository Size</p>
|
||||
<p class="text-black font-semibold">{{ status.repoSize }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3 shadow-sm border border-gray-200">
|
||||
<p class="text-gray-500 font-medium text-xs uppercase">Objects</p>
|
||||
<p class="text-black font-semibold">{{ status.repoObjects }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3 shadow-sm border border-gray-200">
|
||||
<p class="text-gray-500 font-medium text-xs uppercase">Version</p>
|
||||
<p class="text-black font-mono text-sm">{{ status.version }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg p-3 shadow-sm border border-gray-200">
|
||||
<p class="text-gray-500 font-medium text-xs uppercase">Storage Limit</p>
|
||||
<p class="text-black font-semibold">{{ status.storageLimit }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3 mt-4 border border-gray-200">
|
||||
<p class="text-gray-500 text-xs font-medium">Last updated: {{ status.timestamp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 p-8">
|
||||
<div class="space-y-6">
|
||||
<!-- File Drop Zone -->
|
||||
<div class="relative">
|
||||
<label for="fileInput"
|
||||
class="block w-full bg-gray-50 rounded-xl p-12 text-center cursor-pointer border-2 border-dashed border-gray-300 transition-all duration-300 hover:bg-gray-100 hover:border-black group"
|
||||
@dragover.prevent="isDragOver = true" @dragleave.prevent="isDragOver = false"
|
||||
@drop.prevent="handleDrop($event)" :class="{
|
||||
'bg-gray-100 border-black': isDragOver,
|
||||
'bg-green-50 border-green-500': isPasted
|
||||
}">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<i
|
||||
class="fas fa-cloud-upload-alt text-4xl text-gray-400 group-hover:text-black transition-colors duration-300"></i>
|
||||
<div>
|
||||
<span
|
||||
class="text-lg font-semibold text-gray-700 group-hover:text-black transition-colors duration-300">
|
||||
{{ fileLabel }} </span>
|
||||
<p class="text-sm text-gray-500 mt-2">Drag and drop your file here, click to browse, or paste anywhere
|
||||
on the page</p>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<input type="file" id="fileInput" class="hidden" @change="updateFileLabel" />
|
||||
</div>
|
||||
|
||||
<!-- Upload Button -->
|
||||
<button @click="uploadFile" :disabled="uploading || !file"
|
||||
class="w-full bg-black text-white py-3 rounded-xl font-semibold transition-all duration-300 hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<i class="fas fa-upload" :class="{ 'animate-pulse': uploading }"></i>
|
||||
<span v-if="!uploading">Upload</span>
|
||||
<span v-else>Uploading...</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Progress -->
|
||||
<div v-if="uploading" class="text-center animate-fade-in">
|
||||
<div class="bg-white rounded-lg p-6 shadow-lg border border-gray-200">
|
||||
<div class="flex items-center justify-center gap-3 mb-4">
|
||||
<div class="animate-spin">
|
||||
<i class="fas fa-circle-notch text-2xl text-black"></i>
|
||||
</div>
|
||||
<p class="text-black font-semibold text-lg">Uploading to IPFS...</p>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-3 mb-4">
|
||||
<div class="bg-black h-3 rounded-full transition-all duration-300 ease-out"
|
||||
:style="{ width: uploadProgress + '%' }"></div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center text-sm text-gray-600 mb-2">
|
||||
<span>{{ uploadProgress.toFixed(1) }}% complete</span>
|
||||
<span v-if="uploadProgress < 100">Uploading...</span>
|
||||
<span v-else>Processing...</span>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm">Your file is being distributed across the anonymous network</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<div v-if="result" class="mt-6 animate-fade-in">
|
||||
<div :class="[
|
||||
'p-6 rounded-xl shadow-lg border',
|
||||
result.success ? 'bg-white border-gray-200' : 'bg-gray-50 border-gray-300'
|
||||
]">
|
||||
<template v-if="result.success">
|
||||
<!-- Success Status - Moved to top -->
|
||||
<div class="flex items-center gap-3 mb-6 bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-check-circle text-green-600 text-2xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-lg text-green-800">{{ result.message }}</p>
|
||||
<p class="text-sm text-green-600 mt-1">Your file has been successfully uploaded to IPFS</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="flex border-b border-gray-200 mb-4">
|
||||
<button @click="activeTab = 'details'"
|
||||
:class="['tab-button px-4 py-2 text-sm font-medium', activeTab === 'details' ? 'active' : 'text-gray-600 hover:text-gray-800']">
|
||||
File Details
|
||||
</button>
|
||||
<button @click="activeTab = 'urls'"
|
||||
:class="['tab-button px-4 py-2 text-sm font-medium', activeTab === 'urls' ? 'active' : 'text-gray-600 hover:text-gray-800']">
|
||||
Access URLs
|
||||
</button>
|
||||
<button v-if="result.fileType" @click="activeTab = 'embed'"
|
||||
:class="['tab-button px-4 py-2 text-sm font-medium', activeTab === 'embed' ? 'active' : 'text-gray-600 hover:text-gray-800']">
|
||||
Embed Codes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="min-h-[200px]" v-if="result.fileName">
|
||||
<!-- File Details Tab -->
|
||||
<div v-if="activeTab === 'details'" class="space-y-3">
|
||||
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||
<div class="bg-gray-50 rounded p-3">
|
||||
<p class="text-gray-500 text-xs font-medium uppercase">File Name</p>
|
||||
<p class="text-black font-mono text-sm truncate" :title="result.fileName">{{ result.fileName }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded p-3">
|
||||
<p class="text-gray-500 text-xs font-medium uppercase">File Size</p>
|
||||
<p class="text-black font-semibold">{{ result.size }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded p-3">
|
||||
<p class="text-gray-500 text-xs font-medium uppercase">MIME Type</p>
|
||||
<p class="text-black font-mono text-sm">{{ result.mimeType }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded p-3">
|
||||
<p class="text-gray-500 text-xs font-medium uppercase">Media Type</p>
|
||||
<p class="text-black font-semibold">
|
||||
<span v-if="result.fileType"
|
||||
class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-gray-200 text-black">
|
||||
{{ result.fileType }}
|
||||
</span>
|
||||
<span v-else
|
||||
class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-gray-200 text-black">
|
||||
file </span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-span-2 bg-gray-50 rounded p-3">
|
||||
<p class="text-gray-500 text-xs font-medium uppercase">Content Identifier (CID)</p>
|
||||
<p
|
||||
class="text-black font-mono text-sm break-all bg-white rounded px-2 py-1 border border-gray-200 mt-1">
|
||||
{{ result.cid }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Access URLs Tab -->
|
||||
<div v-if="activeTab === 'urls'" class="space-y-3">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="text-xs text-gray-600 font-medium block mb-1">IPFS Protocol</label>
|
||||
<input type="text" :value="getIpfsUrlWithFilename(result.cid, result.fileName)" readonly
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded bg-gray-50 font-mono text-black cursor-pointer hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-black"
|
||||
@click="$event.target.select()" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-600 font-medium block mb-1">IPFS.io Gateway</label>
|
||||
<input type="text"
|
||||
:value="getIpfsUrlWithFilename(result.cid, result.fileName, 'https://ipfs.io/ipfs/')" readonly
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded bg-gray-50 font-mono text-black cursor-pointer hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-black"
|
||||
@click="$event.target.select()" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-600 font-medium block mb-1">Dweb.link Gateway</label>
|
||||
<input type="text"
|
||||
:value="getIpfsUrlWithFilename(result.cid, result.fileName, 'https://dweb.link/ipfs/')"
|
||||
readonly
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded bg-gray-50 font-mono text-black cursor-pointer hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-black"
|
||||
@click="$event.target.select()" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-600 font-medium block mb-1">Network Status</label>
|
||||
<div class="flex gap-2">
|
||||
<a :href="`https://check.ipfs.network/?cid=${result.cid}`" target="_blank"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-black text-white rounded font-medium text-sm hover:bg-gray-800 transition-colors duration-300">
|
||||
Check Network Propagation
|
||||
</a>
|
||||
<a :href="getIpfsUrlWithFilename(result.cid, result.fileName, 'https://dweb.link/ipfs/')"
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 text-white rounded font-medium text-sm hover:bg-gray-700 transition-colors duration-300">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
Open in Dweb.link
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Embed Codes Tab -->
|
||||
<div v-if="activeTab === 'embed' && result.fileType" class="space-y-4">
|
||||
<div>
|
||||
<label class="text-xs text-gray-600 font-medium block mb-1">HTML</label>
|
||||
<textarea :value="getHtmlEmbed(result.cid, result.fileType)" readonly rows="3"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded bg-gray-50 font-mono text-black cursor-pointer hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-black resize-none"
|
||||
@click="$event.target.select()"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-600 font-medium block mb-1">BBCode</label>
|
||||
<textarea :value="getBBCodeEmbed(result.cid, result.fileType)" readonly rows="2"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded bg-gray-50 font-mono text-black cursor-pointer hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-black resize-none"
|
||||
@click="$event.target.select()"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Error Status -->
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-3 bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-exclamation-circle text-red-600 text-2xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-lg text-red-800">{{ result.message }}</p>
|
||||
<p class="text-sm text-red-600 mt-1">Please try uploading your file again</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="bg-black p-4 text-center">
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<a href="https://github.com/besoeasy/file-drop" target="_blank"
|
||||
class="flex items-center gap-2 text-white hover:text-gray-300 transition-colors duration-300">
|
||||
<i class="fab fa-github"></i>
|
||||
<span class="font-semibold">GitHub</span>
|
||||
</a>
|
||||
<span class="text-gray-400">•</span>
|
||||
<span class="text-gray-300 font-mono text-sm">v{{ status.appver }}</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
file: null,
|
||||
fileLabel: "Drop or Choose File",
|
||||
uploading: false,
|
||||
uploadProgress: 0,
|
||||
result: null,
|
||||
isDragOver: false,
|
||||
isPasted: false,
|
||||
activeTab: "details",
|
||||
status: {
|
||||
nodeId: "Loading...",
|
||||
bandwidth: "Loading...",
|
||||
repoSize: "Loading...",
|
||||
repoObjects: "Loading...",
|
||||
version: "Loading...",
|
||||
appver: "Loading...",
|
||||
timestamp: "Loading...",
|
||||
peerscount: 0,
|
||||
storageLimit: "Loading...",
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formatBytes(bytes) {
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + " " + sizes[i];
|
||||
},
|
||||
updateFileLabel(event) {
|
||||
const fileInput = event.target;
|
||||
this.file = fileInput.files[0];
|
||||
this.fileLabel = this.file ? this.file.name : "Drop or Choose File";
|
||||
},
|
||||
handleDrop(event) {
|
||||
this.isDragOver = false;
|
||||
this.file = event.dataTransfer.files[0];
|
||||
this.fileLabel = this.file ? this.file.name : "Drop or Choose File";
|
||||
},
|
||||
handlePaste(event) {
|
||||
console.log("Paste event triggered", event);
|
||||
|
||||
// Check if clipboard contains files
|
||||
const items = event.clipboardData?.items;
|
||||
if (!items) {
|
||||
console.log("No clipboard items found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Clipboard items:", items.length);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
console.log("Item", i, ":", item.kind, item.type);
|
||||
|
||||
if (item.kind === "file") {
|
||||
event.preventDefault();
|
||||
const file = item.getAsFile();
|
||||
console.log("File pasted:", file);
|
||||
|
||||
if (file) {
|
||||
this.file = file;
|
||||
this.fileLabel = file.name || "Pasted File";
|
||||
|
||||
// Show visual feedback for paste
|
||||
this.isPasted = true;
|
||||
setTimeout(() => {
|
||||
this.isPasted = false;
|
||||
}, 1000);
|
||||
|
||||
console.log("File set successfully:", this.file.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getFileTypeIcon(fileType) {
|
||||
switch (fileType) {
|
||||
case "audio":
|
||||
return "fas fa-music";
|
||||
case "video":
|
||||
return "fas fa-video";
|
||||
case "image":
|
||||
return "fas fa-image";
|
||||
default:
|
||||
return "fas fa-file";
|
||||
}
|
||||
},
|
||||
getFileType(mimeType) {
|
||||
if (mimeType.startsWith("audio/")) return "audio";
|
||||
if (mimeType.startsWith("video/")) return "video";
|
||||
if (mimeType.startsWith("image/")) return "image";
|
||||
return null;
|
||||
},
|
||||
getHtmlEmbed(cid, fileType) {
|
||||
const url = `https://ipfs.io/ipfs/${cid}`;
|
||||
switch (fileType) {
|
||||
case "audio":
|
||||
return `<audio controls>\n <source src="${url}" type="audio/mpeg">\n Your browser does not support the audio element.\n</audio>`;
|
||||
case "video":
|
||||
return `<video controls width="640" height="360">\n <source src="${url}" type="video/mp4">\n Your browser does not support the video element.\n</video>`;
|
||||
case "image":
|
||||
return `<img src="${url}" alt="IPFS Image" />`;
|
||||
default:
|
||||
return `<a href="${url}">Download File</a>`;
|
||||
}
|
||||
},
|
||||
getBBCodeEmbed(cid, fileType) {
|
||||
const url = `https://ipfs.io/ipfs/${cid}`;
|
||||
switch (fileType) {
|
||||
case "audio":
|
||||
return `[audio]${url}[/audio]`;
|
||||
case "video":
|
||||
return `[video]${url}[/video]`;
|
||||
case "image":
|
||||
return `[img]${url}[/img]`;
|
||||
default:
|
||||
return `[url=${url}]Download File[/url]`;
|
||||
}
|
||||
},
|
||||
getIpfsUrlWithFilename(cid, filename, gateway = '') {
|
||||
if (!filename) return gateway ? `${gateway}${cid}` : `ipfs://${cid}`;
|
||||
|
||||
const encodedFilename = encodeURIComponent(filename);
|
||||
if (gateway) {
|
||||
return `${gateway}${cid}?filename=${encodedFilename}`;
|
||||
}
|
||||
return `ipfs://${cid}?filename=${encodedFilename}`;
|
||||
},
|
||||
async uploadFile() {
|
||||
if (!this.file) {
|
||||
this.result = { success: false, message: "Please select a file" };
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploading = true;
|
||||
this.uploadProgress = 0;
|
||||
this.result = null;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", this.file);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
// Track upload progress
|
||||
xhr.upload.addEventListener("progress", (event) => {
|
||||
if (event.lengthComputable) {
|
||||
this.uploadProgress = (event.loaded / event.total) * 100;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle completion
|
||||
xhr.addEventListener("load", () => {
|
||||
this.uploading = false;
|
||||
this.uploadProgress = 100;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
|
||||
if (data.status === "success") {
|
||||
this.result = {
|
||||
success: true,
|
||||
message: "Upload Successful",
|
||||
cid: data.cid,
|
||||
size: this.formatBytes(data.size),
|
||||
fileType: this.getFileType(this.file.type),
|
||||
fileName: this.file.name,
|
||||
mimeType: this.file.type || "application/octet-stream",
|
||||
};
|
||||
} else {
|
||||
this.result = {
|
||||
success: false,
|
||||
message: `Error: ${data.error}`,
|
||||
};
|
||||
}
|
||||
resolve(data);
|
||||
} catch (error) {
|
||||
this.result = {
|
||||
success: false,
|
||||
message: "Upload failed. Invalid response from server.",
|
||||
};
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
xhr.addEventListener("error", () => {
|
||||
this.uploading = false;
|
||||
this.uploadProgress = 0;
|
||||
this.result = {
|
||||
success: false,
|
||||
message: "Upload failed. Please check your connection and try again.",
|
||||
};
|
||||
reject(new Error("Upload failed"));
|
||||
});
|
||||
|
||||
// Handle abort
|
||||
xhr.addEventListener("abort", () => {
|
||||
this.uploading = false;
|
||||
this.uploadProgress = 0;
|
||||
this.result = {
|
||||
success: false,
|
||||
message: "Upload was cancelled.",
|
||||
};
|
||||
reject(new Error("Upload cancelled"));
|
||||
});
|
||||
|
||||
// Start the upload
|
||||
xhr.open("POST", "/upload");
|
||||
xhr.send(formData);
|
||||
});
|
||||
},
|
||||
async fetchStatus() {
|
||||
try {
|
||||
const response = await fetch("/status");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "success") {
|
||||
this.status = {
|
||||
nodeId: data.node.id.slice(0, 6) + "..." + data.node.id.slice(-6),
|
||||
bandwidth: `In: ${this.formatBytes(data.bandwidth.totalIn)} | Out: ${this.formatBytes(data.bandwidth.totalOut)}`,
|
||||
repoSize: `${this.formatBytes(data.repository.size)} / ${this.formatBytes(data.repository.storageMax)}`,
|
||||
repoObjects: `${data.repository.numObjects}`,
|
||||
version: `${data.node.agentVersion}`,
|
||||
appver: `${data.appVersion}`,
|
||||
timestamp: new Date(data.timestamp).toLocaleTimeString(),
|
||||
peerscount: data.peers.count,
|
||||
storageLimit: data.storageLimit?.configured || "Unknown",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
this.status = {
|
||||
nodeId: "Error",
|
||||
bandwidth: "Error loading",
|
||||
repoSize: "Error loading",
|
||||
repoObjects: "Error",
|
||||
version: "Error",
|
||||
appver: "Error",
|
||||
timestamp: "Error",
|
||||
peerscount: 0,
|
||||
storageLimit: "Error",
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchStatus();
|
||||
setInterval(this.fetchStatus, 10000);
|
||||
|
||||
// Add global paste event listener
|
||||
document.addEventListener("paste", this.handlePaste);
|
||||
|
||||
// Focus the app to enable paste events
|
||||
this.$el.focus();
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Clean up event listener
|
||||
document.removeEventListener("paste", this.handlePaste);
|
||||
},
|
||||
}).mount("#app");
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user