Skip to content

Commit ec7e107

Browse files
committed
fix: resolve stats logging and vendor directory conflicts
Fix multiple issues with stats command and development workflow by addressing logging permissions and Go vendoring conflicts. Changes: - Rename vendor/js to assets/js to avoid Go vendor detection - Prevents "inconsistent vendoring" errors in task dev - Updates all references in main.go, index.html, Taskfile, Dockerfile - JavaScript assets now in assets/js/ (embedded via go:embed) - Fix stats command logging in Docker environments - Convert logCommand to server method using s.logfile - Fixes disconnect between global logFile and server.logfile - Add directory auto-creation with proper error handling - Add startup logging message showing logfile path - Configure Air for live development - Add .air.toml with -mod=mod flag for Go builds - Exclude assets/ directory from hot reload watching - Fix Docker volume permissions for distroless (UID 65532) - Add chmod 777 logs to docker-run and docker-compose-up tasks - Document permission requirements in README and docker-compose.yaml - Add troubleshooting section for Coolify deployments - Update .gitignore - Add logs/, build-errors.log, access.log - Ensure Air and runtime files are ignored Fixes: - Stats command now works in Docker/docker-compose - task dev no longer fails with vendor directory errors - Proper error messages when log file doesn't exist yet - Automatic log directory creation with proper permissions Security: - Maintains distroless security (no shell, UID 65532) - Documents permission requirements clearly - Auto-fixes permissions in task commands
1 parent 5fd911b commit ec7e107

10 files changed

Lines changed: 63 additions & 30 deletions

File tree

.github/DOCKER.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ docker-compose up
9393
### Trade-offs
9494
-**No debugging** - Can't `docker exec` into container (no shell)
9595
-**No healthcheck command** - Platforms must use external health checks
96-
- ⚠️ **Permission setup** - Volumes must be writable by UID 65532
96+
- ⚠️ **Permission setup** - Volumes must be writable by UID 65532 (especially `/logs` for stats)
9797

9898
### Volumes
9999
- `/data` - Directory for serving files

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ lsget
22
tmp
33
.lsgetignore
44
files
5+
logs
6+
build-errors.log
7+
access.log

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ WORKDIR /build
1010
COPY go.mod go.sum ./
1111
RUN go mod download
1212

13-
# Copy source code and vendored dependencies
13+
# Copy source code and assets
1414
COPY . .
1515

1616
# Build arguments
1717
ARG VERSION=dev
1818

1919
# Build the application with version info
20-
# Static binary with embedded assets (vendor/ only used for JS, not Go)
20+
# Static binary with embedded assets (JS dependencies in assets/js/)
2121
RUN CGO_ENABLED=0 GOOS=linux go build \
2222
-mod=mod \
2323
-ldflags="-w -s -X main.version=${VERSION}" \

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,28 +230,34 @@ The Docker image uses **Google's Distroless base** for maximum security:
230230

231231
**Permission Setup for Volumes:**
232232

233-
Since the container runs as UID 65532 (`nonroot` user), mounted volumes must be writable:
233+
⚠️ **IMPORTANT**: Since the container runs as UID 65532 (`nonroot` user), mounted volumes **must be writable** for:
234+
- `/data` - Serving files (read access sufficient)
235+
- `/logs` - **Writing access logs** (required for `stats` command to work)
234236

235237
```bash
236238
# Create directories with proper permissions
237239
mkdir -p files logs
238240

239-
# Option 1: World-writable (simple, less secure)
240-
chmod 777 files logs
241+
# Option 1: World-writable (simplest, works everywhere)
242+
chmod 777 logs
243+
chmod 755 files # Read-only is fine for serving files
241244

242245
# Option 2: Specific ownership (more secure)
243246
sudo chown -R 65532:65532 files logs
244247

245248
# Option 3: Your user + group write (best for dev)
246249
sudo chown -R $(id -u):$(id -g) files logs
247-
chmod 775 files logs
250+
chmod 775 logs # Needs write for access.log
251+
chmod 755 files
248252
```
249253

250254
**For Coolify/Platform Deployments:**
251255

252-
Most platforms handle permissions automatically. If you encounter issues:
253-
- Coolify: Volume permissions are usually handled by the platform
254-
- Ensure the deployment user has write access to mount paths
256+
⚠️ If `stats` command shows "no activity logged yet":
257+
1. **Check volume permissions** - Logs directory must be writable by UID 65532
258+
2. **Coolify**: Volumes are usually auto-mounted, but check persistent storage settings
259+
3. **Manual fix**: SSH into the server and run `chmod 777 /path/to/mounted/logs`
260+
4. **Verify**: Check server logs for "Logging to: /logs/access.log" message on startup
255261

256262
**Example 1: Simple setup (no baseurl needed):**
257263

Taskfile.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ tasks:
3030
vendor:
3131
desc: "Vendor JavaScript dependencies to reduce supply chain attacks"
3232
cmds:
33-
- mkdir -p vendor/js
34-
- curl -fsSL "https://cdn.jsdelivr.net/npm/marked/marked.min.js" -o vendor/js/marked.min.js
35-
- curl -fsSL "https://cdn.jsdelivr.net/gh/starfederation/datastar@main/bundles/datastar.js" -o vendor/js/datastar.js
36-
- echo "JavaScript dependencies vendored to ./vendor/js/"
33+
- mkdir -p assets/js
34+
- curl -fsSL "https://cdn.jsdelivr.net/npm/marked/marked.min.js" -o assets/js/marked.min.js
35+
- curl -fsSL "https://cdn.jsdelivr.net/gh/starfederation/datastar@main/bundles/datastar.js" -o assets/js/datastar.js
36+
- echo "JavaScript dependencies vendored to ./assets/js/"
3737
docker-build:
3838
desc: "Build Docker image locally for testing"
3939
cmds:
@@ -44,7 +44,9 @@ tasks:
4444
deps: [docker-build]
4545
cmds:
4646
- mkdir -p files logs
47+
- chmod 777 logs # Container runs as UID 65532, needs write access
4748
- echo "Starting lsget container on http://localhost:8080"
49+
- echo "Log file ./logs/access.log"
4850
- docker run --rm -it -p 8080:8080 -v $(pwd)/files:/data -v $(pwd)/logs:/logs -e LSGET_ADDR=0.0.0.0:8080 -e LSGET_DIR=/data -e LSGET_LOGFILE=/logs/access.log lsget:local
4951
docker-test:
5052
desc: "Build and test Docker image (version check)"
@@ -56,6 +58,7 @@ tasks:
5658
desc: "Start services with docker-compose"
5759
cmds:
5860
- mkdir -p files logs
61+
- chmod 777 logs # Container runs as UID 65532, needs write access
5962
- docker-compose up -d
6063
- echo "Services started. Access at http://localhost:8080"
6164
- echo "View logs with task docker-compose-logs"

docker-compose.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ services:
22
lsget:
33
build: .
44
container_name: lsget
5+
# IMPORTANT: Volumes must be writable by UID 65532 (nonroot user)
6+
# Before first run: mkdir -p files logs && chmod 777 logs
57
volumes:
68
- ./files:/data
79
- ./logs:/logs

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,12 @@
252252
};
253253
</script>
254254

255-
<script src="/vendor/js/marked.min.js"></script>
255+
<script src="/assets/js/marked.min.js"></script>
256256

257257
<!-- Datastar: Alpine-like data-* reactivity -->
258258
<script
259259
type="module"
260-
src="/vendor/js/datastar.js"
260+
src="/assets/js/datastar.js"
261261
></script>
262262

263263
<style>

main.go

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,10 @@ func readDocFile(dir string) (string, string) {
311311
//go:embed index.html
312312
var embeddedIndex []byte
313313

314-
//go:embed vendor/js/marked.min.js
314+
//go:embed assets/js/marked.min.js
315315
var embeddedMarkedJS []byte
316316

317-
//go:embed vendor/js/datastar.js
317+
//go:embed assets/js/datastar.js
318318
var embeddedDatastarJS []byte
319319

320320
// ===== Server state =====
@@ -599,15 +599,21 @@ func getClientIP(r *http.Request) string {
599599
}
600600

601601
// logCommand writes a command execution to the log file
602-
func logCommand(cmd, filePath, ip string) {
603-
if logFile == "" {
602+
func (s *server) logCommand(cmd, filePath, ip string) {
603+
if s.logfile == "" {
604604
return
605605
}
606606

607607
logMutex.Lock()
608608
defer logMutex.Unlock()
609609

610-
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
610+
// Ensure log directory exists
611+
logDir := filepath.Dir(s.logfile)
612+
if err := os.MkdirAll(logDir, 0755); err != nil {
613+
return
614+
}
615+
616+
f, err := os.OpenFile(s.logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
611617
if err != nil {
612618
return
613619
}
@@ -1439,13 +1445,13 @@ func (s *server) handleExec(w http.ResponseWriter, r *http.Request) {
14391445
}
14401446
if len(files) == 1 {
14411447
// Single file, download directly
1442-
logCommand("get", files[0].virtualPath, ip)
1448+
s.logCommand("get", files[0].virtualPath, ip)
14431449
url := "/api/download?path=" + urlEscapeVirtual(files[0].virtualPath)
14441450
_ = json.NewEncoder(w).Encode(execResp{Output: "", Download: url})
14451451
return
14461452
}
14471453
// Multiple files, create zip
1448-
logCommand("get", "(pattern match)", ip)
1454+
s.logCommand("get", "(pattern match)", ip)
14491455
url := "/api/download?pattern=" + urlQueryEscape(pattern) + "&cwd=" + urlEscapeVirtual(sess.cwd)
14501456
_ = json.NewEncoder(w).Encode(execResp{Output: fmt.Sprintf("Downloading %d files as archive.zip", len(files)), Download: url})
14511457
return
@@ -1476,14 +1482,14 @@ func (s *server) handleExec(w http.ResponseWriter, r *http.Request) {
14761482
return
14771483
}
14781484
dirName := filepath.Base(rp)
1479-
logCommand("get", vp+" (dir)", ip)
1485+
s.logCommand("get", vp+" (dir)", ip)
14801486
url := "/api/download?dir=" + urlEscapeVirtual(vp)
14811487
_ = json.NewEncoder(w).Encode(execResp{Output: fmt.Sprintf("Downloading directory '%s' with %d files as %s.zip", dirName, len(files), dirName), Download: url})
14821488
return
14831489
}
14841490

14851491
// Single file download
1486-
logCommand("get", vp, ip)
1492+
s.logCommand("get", vp, ip)
14871493
url := "/api/download?path=" + urlEscapeVirtual(vp)
14881494
_ = json.NewEncoder(w).Encode(execResp{Output: "", Download: url})
14891495
return
@@ -1648,7 +1654,7 @@ func (s *server) handleExec(w http.ResponseWriter, r *http.Request) {
16481654
}
16491655

16501656
// Log the share command
1651-
logCommand(cmd, vp, getClientIP(r))
1657+
s.logCommand(cmd, vp, getClientIP(r))
16521658

16531659
// Return the URL with clipboard instruction
16541660
_ = json.NewEncoder(w).Encode(execResp{
@@ -1795,7 +1801,7 @@ func (s *server) handleExec(w http.ResponseWriter, r *http.Request) {
17951801
sha256Sum := hex.EncodeToString(sha256Hash.Sum(nil))
17961802

17971803
// Log the checksum command
1798-
logCommand(cmd, vp, getClientIP(r))
1804+
s.logCommand(cmd, vp, getClientIP(r))
17991805

18001806
output := fmt.Sprintf("MD5: %s\nSHA256: %s", md5Sum, sha256Sum)
18011807
_ = json.NewEncoder(w).Encode(execResp{Output: output})
@@ -1809,7 +1815,11 @@ func (s *server) handleExec(w http.ResponseWriter, r *http.Request) {
18091815

18101816
stats, err := parseLogStats(s.logfile)
18111817
if err != nil {
1812-
_ = json.NewEncoder(w).Encode(execResp{Output: fmt.Sprintf("stats: error reading log file: %v", err)})
1818+
if os.IsNotExist(err) {
1819+
_ = json.NewEncoder(w).Encode(execResp{Output: fmt.Sprintf("stats: no activity logged yet\nLog file: %s\nStatistics will be available once files are shared, downloaded, or checksummed.", s.logfile)})
1820+
} else {
1821+
_ = json.NewEncoder(w).Encode(execResp{Output: fmt.Sprintf("stats: error reading log file: %v", err)})
1822+
}
18131823
return
18141824
}
18151825

@@ -2989,11 +2999,16 @@ func main() {
29892999
mux.HandleFunc("/api/static/", s.handleStaticFile)
29903000
mux.HandleFunc("/sitemap.xml", s.handleSitemap)
29913001
// Vendored JavaScript dependencies
2992-
mux.HandleFunc("/vendor/js/marked.min.js", s.handleVendoredMarked)
2993-
mux.HandleFunc("/vendor/js/datastar.js", s.handleVendoredDatastar)
3002+
mux.HandleFunc("/assets/js/marked.min.js", s.handleVendoredMarked)
3003+
mux.HandleFunc("/assets/js/datastar.js", s.handleVendoredDatastar)
29943004
mux.HandleFunc("/", s.handleIndex) // Catch-all route must be last
29953005

29963006
fmt.Printf("Serving %s on http://%s (cat max = %d bytes)\n", rootAbs, *addr, *catMax)
3007+
if s.logfile != "" {
3008+
fmt.Printf("Logging to: %s\n", s.logfile)
3009+
} else {
3010+
fmt.Println("Logging disabled (use -logfile or LSGET_LOGFILE to enable)")
3011+
}
29973012
srv := &http.Server{
29983013
Addr: *addr,
29993014
Handler: logRequests(mux),
@@ -3099,6 +3114,10 @@ func logRequests(next http.Handler) http.Handler {
30993114
// Write to log file if specified
31003115
if logFile != "" {
31013116
logMutex.Lock()
3117+
// Ensure log directory exists
3118+
if logDir := filepath.Dir(logFile); logDir != "" {
3119+
_ = os.MkdirAll(logDir, 0755)
3120+
}
31023121
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
31033122
if err == nil {
31043123
_, _ = f.WriteString(logLine)

0 commit comments

Comments
 (0)