Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# ==========================================
# STAGE 1: The Builder (Heavy, used only for compiling)
# ==========================================
FROM node:20 AS builder

RUN corepack enable
WORKDIR /app

# Install git
RUN apt-get update && \
apt-get install -y --no-install-recommends git && \
rm -rf /var/lib/apt/lists/*

# Git Clone branch and repository
RUN git clone https://github.com/mifi/ezshare.git .

# Copy the source code (enable this if using for development mode - you'll need to have local copy of the code)
#COPY ./ ./

# Install ALL dependencies (including dev tools like TypeScript) and build
RUN yarn install --immutable
RUN yarn build

# ==========================================
# STAGE 2: The Runner (Lightweight, final production image)
# ==========================================
# We use node:20-slim here, which is hundreds of MBs smaller!
FROM node:20-slim AS runner

# Install ffmpeg, xsel, xvfb and immediately clean up the apt cache to save space
RUN apt-get update && \
apt-get install --no-install-recommends -y xvfb ffmpeg xsel dbus && \
rm -rf /var/lib/apt/lists/*

RUN corepack enable
WORKDIR /app

# Copy ONLY the necessary files from the "builder" stage
COPY --from=builder /app/package.json ./
COPY --from=builder /app/yarn.lock ./
COPY --from=builder /app/.yarnrc.yml ./
COPY --from=builder /app/.yarn ./.yarn

# Copy the packages folder (which now contains the built "dist" folders)
COPY --from=builder /app/packages ./packages

# Optional but recommended: Remove the heavy "src" folders since we only need "dist" now
RUN rm -rf packages/*/src

# Install ONLY production dependencies (ignores devDependencies) and clean Yarn cache
RUN yarn workspaces focus --production $(node -p "require('./packages/cli/package.json').name") && yarn cache clean --all

#To have fake display
RUN echo '#!/bin/bash\n\
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &\n\
export DISPLAY=:99\n\
sleep 1\n\
exec dbus-run-session -- "$@"' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh

# Create the shared directory
RUN mkdir /shared

# --- SECURITY CHANGES START ---
# 1. Change ownership of /app and /shared to the 'node' user (UID 1000)
# This ensures the user can write to these locations.
RUN chown -R node:node /app /shared

# 2. Switch to non-root user
USER node
# --- SECURITY CHANGES END ---

# Expose port and start
EXPOSE 3003
CMD ["/app/entrypoint.sh", "node", "packages/cli/dist/index.js", "/shared", "--port", "3003"]
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
services:
ezshare:
build:
context: .
dockerfile: Dockerfile
container_name: ezshare-app
restart: unless-stopped

# Map port 3003 on host to 3003 in container
ports:
- "3003:3003"

# Mount a local directory to /shared
volumes:
- ./shared/my/path:/shared

# (Optional) Ensure the container uses the node user explicitly
# though the Dockerfile USER instruction already handles this.
user: "node"
2 changes: 2 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import assert from 'node:assert';
import { dirname } from 'node:path';
import qrcode from 'qrcode-terminal';
import { fileURLToPath } from 'node:url';
//import fs from 'fs';
//import path from 'path';

import App, { parseArgs } from '@ezshare/lib';

Expand Down
63 changes: 63 additions & 0 deletions packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,69 @@ export default ({ sharedPath: sharedPathIn, port, maxUploadSize, zipCompressionL
});
}));

app.delete('/api/delete', asyncHandler(async (req, res) => {
const { path: filePath } = req.query;

// Ensure path is provided and is a string
assert(typeof filePath === 'string', 'Path must be a string');

// Use existing helper to resolve path and check security (prevents directory traversal)
const absPath = await getFileAbsPath(filePath);

// prevent deleting the root shared folder
if (absPath === sharedPath) {
res.status(403).json({ error: 'Cannot delete root directory' });
return;
}

console.log('Deleting item:', absPath);

// Use fs.rm instead of fs.unlink to handle both files and non-empty directories safely
await fs.rm(absPath, { recursive: true, force: true });

res.json({ success: true });
}));

// NEW CREATE DIRECTORY ENDPOINT
app.post('/api/mkdir', bodyParser.json(), asyncHandler(async (req, res) => {
const { path: parentPath, name } = req.body;

assert(typeof parentPath === 'string', 'Parent path must be a string');
assert(typeof name === 'string', 'Folder name must be a string');

const absParentPath = await getFileAbsPath(parentPath);
const safeName = filenamify(name, { maxLength: 255 });
const newDirPath = join(absParentPath, safeName);

console.log('Creating directory:', newDirPath);
await fs.mkdir(newDirPath);

res.json({ success: true });
}));

// NEW RENAME FILE/FOLDER ENDPOINT
app.post('/api/rename', bodyParser.json(), asyncHandler(async (req, res) => {
const { path: targetPath, newName } = req.body;

assert(typeof targetPath === 'string', 'Target path must be a string');
assert(typeof newName === 'string', 'New name must be a string');

const absOldPath = await getFileAbsPath(targetPath);

if (absOldPath === sharedPath) {
res.status(403).json({ error: 'Cannot rename root directory' });
return;
}

const parentDir = join(absOldPath, '..');
const safeNewName = filenamify(newName, { maxLength: 255 });
const absNewPath = join(parentDir, safeNewName);

console.log(`Renaming ${absOldPath} to ${absNewPath}`);
await fs.rename(absOldPath, absNewPath);

res.json({ success: true });
}));
// NOTE: Must support non latin characters
app.post('/api/paste', bodyParser.urlencoded({ extended: false }), asyncHandler(async (req, res) => {
// eslint-disable-next-line unicorn/prefer-ternary
Expand Down
Loading