Compare commits

...

4 commits

Author SHA1 Message Date
5f6a88fbbc
wiki location correction and more
- readme changes and bashscripts moved/organized better
Co-authored-by: riomoo <alister@kamikishi.net>
Co-committed-by: riomoo <alister@kamikishi.net>
2026-02-04 02:17:30 -05:00
f0efc83cf6
declaring 1.3.0
Package Release Number

better licence image

Shrunk image

Fixed size

MASCOT UPDATE

fixed CSS and Added Mascot to main site

allows Enter to Register/Login

changed jpeg cover quality from 70 to 85

Wink over form

more CSS fixes

linked wiki

corrected Readme

Reviewed-on: #8
Co-authored-by: riomoo <alister@kamikishi.net>
Co-committed-by: riomoo <alister@kamikishi.net>

removed ico from LFS

fixing ico

Reviewed-on: #9
Co-authored-by: riomoo <alister@kamikishi.net>
Co-committed-by: riomoo <alister@kamikishi.net>

fixing lfs

Reviewed-on: #10
Co-authored-by: riomoo <alister@kamikishi.net>
Co-committed-by: riomoo <alister@kamikishi.net>
2026-02-02 06:23:55 -05:00
05a1723805
feat: Environment Variables added and more
Reviewed-on: #6
2026-01-19 02:41:55 -05:00
bd8437bd1f
Environment Variables added
Include Windows Build option

containerignore updated

readme Environment Variable Additions

Added Building for windows AND linux

- using podman or docker
2026-01-18 23:41:38 -05:00
24 changed files with 594 additions and 49 deletions

View file

@ -2,11 +2,15 @@
.git
.gitignore
.gitattributes
bash-scripts
scripts-bash
scripts-go
Containerfile
Containerfile.build
binaries
README.md
xml-template
cache
etc
library
watch
releases

7
.gitattributes vendored
View file

@ -1,8 +1,5 @@
*.jpg filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.lua filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text
@ -31,7 +28,3 @@
*.odt filter=lfs diff=lfs merge=lfs -text
*.docx filter=lfs diff=lfs merge=lfs -text
*.apk filter=lfs diff=lfs merge=lfs -text
*.ico filter=lfs diff=lfs merge=lfs -text
*.JXL filter=lfs diff=lfs merge=lfs -text
*.AVIF filter=lfs diff=lfs merge=lfs -text
*.PNG filter=lfs diff=lfs merge=lfs -text

2
.gitignore vendored
View file

@ -7,3 +7,5 @@ cache
library
etc
watch
binaries
releases

View file

@ -25,11 +25,11 @@ RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build \
-a \
-ldflags="-s -w -linkmode external -extldflags '-static' -X main.GOMEMLIMIT=512MiB -X runtime.defaultGOGC=50" \
-trimpath \
-o bin/main app/gopherbook/main.go
-o bin/main ./app/gopherbook
RUN upx --best --ultra-brute bin/main
RUN chmod +x bin/main
FROM cgr.dev/chainguard/static:latest
FROM git.jester-designs.com/riomoo/alisterbase:1.0.0
WORKDIR /app

55
Containerfile.build Normal file
View file

@ -0,0 +1,55 @@
# Multi-platform build container for Gopherbook
FROM golang:alpine AS builder
RUN apk add --no-cache \
musl-dev \
gcc \
g++ \
mingw-w64-gcc \
wget \
xz \
git
# Install UPX for binary compression
RUN wget https://github.com/upx/upx/releases/download/v5.0.2/upx-5.0.2-amd64_linux.tar.xz && \
tar -xf upx-5.0.2-amd64_linux.tar.xz && \
mv upx-5.0.2-amd64_linux/upx /usr/local/bin/upx && \
rm -r upx-5.0.2-amd64_linux upx-5.0.2-amd64_linux.tar.xz
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build Linux binary
RUN echo "Building Linux binary..." && \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build \
-a \
-ldflags="-s -w -linkmode external -extldflags '-static'" \
-trimpath \
-o bin/gopherbook-linux ./app/gopherbook && \
upx --best --ultra-brute bin/gopherbook-linux && \
chmod +x bin/gopherbook-linux
# Build Windows binary
RUN echo "Building Windows binary..." && \
echo 'IDI_ICON1 ICON "./app/gopherbook/static/images/favicon/favicon.ico"' > gopherbook.rc && \
x86_64-w64-mingw32-windres gopherbook.rc -o ./app/gopherbook/gopherbook.syso && \
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc go build \
-a \
-ldflags="-s -w" \
-trimpath \
-o bin/gopherbook-windows.exe ./app/gopherbook && \
upx --best --ultra-brute bin/gopherbook-windows.exe && \
rm ./app/gopherbook/gopherbook.syso gopherbook.rc
# Verify binaries were created
RUN ls -lh bin/ && \
echo "Build complete!" && \
echo "Linux binary size: $(du -h bin/gopherbook-linux | cut -f1)" && \
echo "Windows binary size: $(du -h bin/gopherbook-windows.exe | cut -f1)"
# Keep the builder stage as the final stage so we can copy files out
FROM builder

View file

@ -1,5 +1,5 @@
build:
go build -o bin/main app/gopherbook/main.go
go build -o bin/main ./app/gopherbook
clean:
rm -rf watch etc library cache
rm -rf watch etc library cache binaries releases

View file

@ -1,11 +1,21 @@
# Gopherbook Self-Hosted Comic Library & CBZ/CBT Reader
<div align="center">
<img src="https://git.jester-designs.com/riomoo/gopherbook/media/branch/main/docs/images/gopherbook-title.png" alt="Description" width="50%">
</div>
<div align="center">
## Self-Hosted Comic Library & CBZ/CBT Reader
Gopherbook is a lightweight, single-binary, self-hosted web comic reader and library manager written in Go.
It is designed for people who want full control over their digital comic collection (CBZ/CBT files), including support for password-protected/encrypted archives, per-user libraries, tagging, automatic organization, and a clean modern reader.
</div>
## License
[![Custom badge](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fshare.jester-designs.com%2Fview%2Fpil.json)](LICENSE)
[![Custom badge](https://git.jester-designs.com/riomoo/gopherbook/media/branch/main/docs/images/svgs/PIL.svg)](LICENSE)
## Features
@ -56,7 +66,16 @@ Then open http://localhost:8080 in your browser.
```bash
git clone https://codeberg.org/riomoo/gopherbook.git
cd gopherbook
./scripts-bash/run.sh
./scripts-bash/run-gb-container/run-podman.sh
```
or
## If you want to use this with docker:
```bash
git clone https://codeberg.org/riomoo/gopherbook.git
cd gopherbook
./scripts-bash/run-gb-container/run-docker.sh
```
Then open http://localhost:12010 in your browser.
@ -78,6 +97,15 @@ Then open http://localhost:12010 in your browser.
./etc/admin.json ← admin settings (registration toggle)
```
## Environment Variables
Can be used to set where everything is stored with:
```
GOPHERBOOK_LIBRARY=$HOME/.config/gopherbook/library
GOPHERBOOK_CACHE=$HOME/.config/gopherbook/covers
GOPHERBOOK_ETC=$HOME/.config/gopherbook/etc
GOPHERBOOK_WATCH=$HOME/.config/gopherbook/watch
```
## Watch folder for bulk imports
Gopherbook includes an automatic watch folder system that makes bulk importing comics effortless:
@ -177,6 +205,14 @@ Pull requests are welcome! Especially:
Please open an issue first for bigger changes.
## If you'd like to know about the new mascot Vinny
- Check the Wiki!
- **GitHub**: https://github.com/riomoo/gopherbook/wiki/Meet-Vinny
- **GitGud**: https://gitgud.io/riomoo/gopherbook/-/wikis/Meet-Vinny
- **JesterDesigns**: https://git.jester-designs.com/riomoo/gopherbook/wiki/Meet-Vinny
## Thanks / Credits
- yeka/zip password-protected ZIP support in pure Go

39
app/gopherbook/config.go Normal file
View file

@ -0,0 +1,39 @@
package main
import (
"os"
"path/filepath"
)
var (
baseLibraryPath string
baseCachePath string
baseEtcPath string
baseWatchPath string
)
func init() {
// Store the base paths (before user-specific paths are added)
baseLibraryPath = libraryPath
baseCachePath = cachePath
baseEtcPath = etcPath
baseWatchPath = watchPath
// Override from environment variables if set
if env := os.Getenv("GOPHERBOOK_LIBRARY"); env != "" {
baseLibraryPath = filepath.Clean(env)
libraryPath = baseLibraryPath
}
if env := os.Getenv("GOPHERBOOK_CACHE"); env != "" {
baseCachePath = filepath.Clean(env)
cachePath = baseCachePath
}
if env := os.Getenv("GOPHERBOOK_ETC"); env != "" {
baseEtcPath = filepath.Clean(env)
etcPath = baseEtcPath
}
if env := os.Getenv("GOPHERBOOK_WATCH"); env != "" {
baseWatchPath = filepath.Clean(env)
watchPath = baseWatchPath
}
}

View file

@ -21,6 +21,7 @@ import (
"regexp"
"sort"
"strings"
"io/fs"
"sync"
"time"
"runtime"
@ -39,6 +40,9 @@ import (
//go:embed templates/index.html
var templateFS embed.FS
//go:embed all:static
var staticFS embed.FS
// ComicInfo represents the standard ComicInfo.xml metadata
type ComicInfo struct {
XMLName xml.Name `xml:"ComicInfo"`
@ -135,6 +139,14 @@ func main() {
loadUsers()
initWatchFolders()
// Create static sub-filesystem once
staticSubFS, err := fs.Sub(staticFS, "static")
if err != nil {
log.Fatal(fmt.Errorf("failed to create static sub-filesystem: %w", err))
}
// Create handlers once and reuse
staticHandler := http.FileServer(http.FS(staticSubFS))
http.HandleFunc("/api/register", handleRegister)
http.HandleFunc("/api/login", handleLogin)
@ -154,6 +166,7 @@ func main() {
http.HandleFunc("/api/admin/delete-comic/", authMiddleware(handleDeleteComic))
http.HandleFunc("/api/watch-folder", authMiddleware(handleWatchFolder))
http.HandleFunc("/", serveUI)
http.Handle("/static/", http.StripPrefix("/static/", staticHandler))
go func() {
for {
@ -544,8 +557,8 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
currentUser = req.Username
key := deriveKey(req.Password)
libraryPath = filepath.Join("./library", currentUser)
cachePath = filepath.Join("./cache/covers", currentUser)
libraryPath = filepath.Join(baseLibraryPath, currentUser)
cachePath = filepath.Join(baseCachePath, currentUser)
os.MkdirAll(filepath.Join(libraryPath, "Unorganized"), 0755)
os.MkdirAll(cachePath, 0755)
@ -599,8 +612,8 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
passwordsMutex.Unlock()
currentEncryptionKey = nil
currentUser = ""
libraryPath = "./library"
cachePath = "./cache/covers"
libraryPath = baseLibraryPath
cachePath = baseCachePath
http.SetCookie(w, &http.Cookie{
Name: "session",
@ -1599,7 +1612,7 @@ func saveJPEG(img image.Image, path string) error {
defer out.Close()
// Lower quality = smaller memory footprint during encoding
err = jpeg.Encode(out, img, &jpeg.Options{Quality: 70})
err = jpeg.Encode(out, img, &jpeg.Options{Quality: 85})
img = nil
runtime.GC()
@ -1643,7 +1656,7 @@ func handleTags(w http.ResponseWriter, r *http.Request) {
}
if req.Color == "" {
req.Color = "#1f6feb"
req.Color = "#446B6E"
}
tagsMutex.Lock()
@ -1832,7 +1845,7 @@ func handleTryKnownPasswords(w http.ResponseWriter, r *http.Request) {
tagData.Count++
tags[tag] = tagData
} else {
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1}
tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
}
}
tagsMutex.Unlock()
@ -1947,7 +1960,7 @@ func handleSetPassword(w http.ResponseWriter, r *http.Request) {
tagData.Count++
tags[tag] = tagData
} else {
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1}
tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
}
}
tagsMutex.Unlock()
@ -2419,7 +2432,7 @@ func processComic(filePath, filename string, modTime time.Time) Comic {
tagData.Count++
tags[tag] = tagData
} else {
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1}
tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
}
}
tagsMutex.Unlock()
@ -2481,7 +2494,7 @@ func loadComicMetadataLazy(comicID string) error {
tagData.Count++
tags[tag] = tagData
} else {
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1}
tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
}
}
tagsMutex.Unlock()
@ -2697,9 +2710,16 @@ func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
next(w, r)
}
}
func getUsersPath() string {
return filepath.Join(etcPath, "users.json")
}
func getAdminPath() string {
return filepath.Join(etcPath, "admin.json")
}
func loadUsers() {
data, err := os.ReadFile("etc/users.json")
data, err := os.ReadFile(getUsersPath())
if err != nil {
return
}
@ -2707,7 +2727,7 @@ func loadUsers() {
log.Printf("Error unmarshaling users: %v", err)
}
adminData, err := os.ReadFile("etc/admin.json")
adminData, err := os.ReadFile(getAdminPath())
if err == nil && len(adminData) > 0 {
var adminConfig struct{ RegistrationEnabled bool }
if err := json.Unmarshal(adminData, &adminConfig); err == nil {
@ -2718,13 +2738,13 @@ func loadUsers() {
func saveUsers() {
data, _ := json.MarshalIndent(users, "", " ")
os.WriteFile("etc/users.json", data, 0644)
os.WriteFile(getUsersPath(), data, 0644)
}
func saveAdminConfig() {
config := struct{ RegistrationEnabled bool }{RegistrationEnabled: registrationEnabled}
data, _ := json.MarshalIndent(config, "", " ")
os.WriteFile("etc/admin.json", data, 0644)
os.WriteFile(getAdminPath(), data, 0644)
}
func loadTags() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

View file

@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/static/images/favicon/favicon.ico" rel="shortcut icon" type="image/x-icon">
<title>Gopherbook</title>
<style>
* {
@ -27,7 +28,7 @@
header {
background: #395E62;
border-bottom: 1px solid #314C52;
padding: 20px 0;
padding: 10px 0;
margin-bottom: 30px;
}
@ -43,6 +44,8 @@
h1 {
color: #1b1e2c;
font-size: 24px;
padding: 20px 10px;
margin: auto;
}
.auth-section {
@ -243,7 +246,7 @@
.comic-artist {
display: inline-block;
background: #1f6feb;
background: #446B6E;
color: white;
padding: 2px 8px;
border-radius: 12px;
@ -275,10 +278,18 @@
width: auto;
}
.tab:hover {
color: #1b1e2c;
}
.tab.active {
color: #395E62;
border-bottom-color: #395E62;
}
.tab.active:hover {
color: #1b1e2c;
border-bottom-color: #395E62;
}
.message {
padding: 12px;
@ -334,7 +345,7 @@
}
.bookmark-indicator {
color: #f0883e;
color: #395E62;
font-size: 14px;
display: flex;
align-items: center;
@ -345,7 +356,7 @@
position: absolute;
top: 8px;
right: 8px;
background: #f0883e;
background: #395E62;
color: white;
padding: 4px 8px;
border-radius: 4px;
@ -370,7 +381,7 @@
}
.bookmark-list-title {
color: #f0883e;
color: #395E62;
font-weight: 600;
margin-bottom: 8px;
font-size: 14px;
@ -394,8 +405,7 @@
}
.bookmark-list-item.current {
border-color: #f0883e;
background: rgba(240, 136, 62, 0.1);
border-color: #395E62;
}
.bookmark-delete {
@ -539,7 +549,7 @@
}
.modal-title {
color: #58a6ff;
color: #446B6E;
font-size: 18px;
font-weight: 600;
}
@ -667,7 +677,7 @@
}
.password-modal-title {
color: #58a6ff;
color: #446B6E;
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
@ -701,7 +711,7 @@
.password-input-group input:focus {
outline: none;
border-color: #58a6ff;
border-color: #446B6E;
}
.password-modal-buttons {
@ -726,16 +736,22 @@
<body>
<header>
<div class="header-content">
<h1>Gopherbook</h1>
<div id="userInfo" class="hidden">
<button onclick="logout()" class="secondary-btn" style="width: auto; padding: 8px 16px;">Logout</button>
</div>
<div style="display: flex;">
<img src="/static/images/pngs/CutePose2.png" alt="on book" width="67px" height="75px">
<h1>Gopherbook</h1>
</div>
<div id="userInfo" class="hidden">
<button onclick="logout()" class="secondary-btn" style="width: auto; padding: 8px 16px;">Logout</button>
</div>
</div>
</header>
<div class="container">
<div id="authSection" class="auth-section">
<div id="authMessage" class="message hidden"></div>
<div style="display: flex; justify-content: center; margin: 0px 10px 20px 10px;">
<img src="/static/images/pngs/LogoPose2.png" alt="wink" style="width: 100px; height auto;">
</div>
<div class="tabs">
<button class="tab active" onclick="showTab('login')">Login</button>
<button class="tab" onclick="showTab('register')">Register</button>
@ -859,7 +875,7 @@
<h4 style="font-size: 14px; color: #8b949e; margin-bottom: 12px;">Create New Tag</h4>
<div class="add-tag-form">
<input type="text" id="newTagName" class="add-tag-input" placeholder="Tag name">
<input type="color" id="newTagColor" class="color-picker" value="#1f6feb">
<input type="color" id="newTagColor" class="color-picker" value="#446B6E">
<button onclick="createTag()" style="width: auto; padding: 8px 16px;">Add</button>
</div>
</div>
@ -1052,7 +1068,7 @@
selectedTags.forEach(function(tagName) {
var tag = allTags.find(function(t) { return t.name === tagName; });
var color = tag ? tag.color : '#1f6feb';
var color = tag ? tag.color : '#446B6E';
var filter = document.createElement('div');
filter.className = 'tag-filter';
filter.style.background = color;
@ -1200,7 +1216,7 @@
tagsHTML = '<div class="comic-tags">';
comic.tags.forEach(tagName => {
const tag = allTags.find(t => t.name === tagName);
const color = tag ? tag.color : '#1f6feb';
const color = tag ? tag.color : '#446B6E';
tagsHTML += '<span class="comic-tag" style="background: ' + color + '">' + tagName + '</span>';
});
tagsHTML += '</div>';
@ -1279,7 +1295,7 @@
managingComic.tags.forEach(tagName => {
const tag = allTags.find(t => t.name === tagName);
const color = tag ? tag.color : '#1f6feb';
const color = tag ? tag.color : '#446B6E';
const item = document.createElement('div');
item.className = 'tag-item';
@ -1367,7 +1383,7 @@
if (res.ok) {
document.getElementById('newTagName').value = '';
document.getElementById('newTagColor').value = '#1f6feb';
document.getElementById('newTagColor').value = '#446B6E';
loadTags();
renderAvailableTags();
showMessage('Tag created!', 'success');
@ -1986,6 +2002,30 @@
.catch(function(err) {
console.error('Initial comics fetch failed:', err);
});
// Add Enter key support for login form
document.getElementById('loginUsername').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
login();
}
});
document.getElementById('loginPassword').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
login();
}
});
// Add Enter key support for register form
document.getElementById('regUsername').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
register();
}
});
document.getElementById('regPassword').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
register();
}
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

10
docs/images/svgs/PIL.svg Normal file
View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="114" height="28">
<g shape-rendering="crispEdges">
<rect width="75" height="28" fill="#555"/>
<rect x="75" width="39" height="28" fill="#07124A"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="10" font-weight="bold">
<text x="37" y="18">LICENSE</text>
<text x="94" y="18">PIL</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 447 B

View file

@ -0,0 +1,77 @@
#!/bin/bash
IMAGE_NAME="localhost/gopherbook:latest"
CONTAINER_NAME="gopherbook"
VERSION="1.3.000"
BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
VCS_REF=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
echo "Building new image: $IMAGE_NAME..."
podman build --force-rm -t "${IMAGE_NAME}-temp" \
--format oci \
-f Containerfile .
if [ $? -ne 0 ]; then
echo "Image build failed. Exiting script."
exit 1
fi
# Ensure directories exist with correct permissions
mkdir -p ./library ./cache ./etc ./watch
if podman container exists "$CONTAINER_NAME"; then
echo "Container '$CONTAINER_NAME' already exists. Stopping and removing it..."
podman stop "$CONTAINER_NAME"
podman rm "$CONTAINER_NAME"
fi
echo "Creating final image with labels in config (no layer)..."
# Use buildah to modify the image config directly
buildah from --name working-container "${IMAGE_NAME}-temp"
# Add labels directly to the config (doesn't create a layer!)
buildah config \
--label "org.opencontainers.image.title=Gopherbook" \
--label "org.opencontainers.image.description=Gopherbook Minimal Image" \
--label "org.opencontainers.image.version=${VERSION}" \
--label "org.opencontainers.image.created=${BUILD_DATE}" \
--label "org.opencontainers.image.revision=${VCS_REF}" \
--label "org.opencontainers.image.authors=Alister <alister@kamikishi.net>" \
--label "org.opencontainers.image.vendor=Jester Designs" \
--label "org.opencontainers.image.licenses=PIL" \
--label "com.jesterdesigns.image.type=base-image" \
--label "com.jesterdesigns.image.purpose=static-binary-runtime" \
working-container
# Commit the container to the final image name
buildah commit --format oci working-container "$IMAGE_NAME"
# Cleanup
buildah rm working-container
podman rmi "${IMAGE_NAME}-temp"
echo "Starting new container from image: $IMAGE_NAME..."
# IMPROVED: Better memory settings and limits
podman run -d --name "$CONTAINER_NAME" \
--memory=512m \
--restart unless-stopped \
-p 12010:8080 \
-v ./library:/app/library \
-v ./cache:/app/cache \
-v ./etc:/app/etc \
-v ./watch:/app/watch \
"$IMAGE_NAME"
if [ $? -ne 0 ]; then
echo "Failed to start new container. Exiting script."
exit 1
fi
echo "Cleaning up old images..."
podman image prune --force
echo "Update and cleanup complete!"
echo "Container is running with memory limit: 512MB"
echo "Go memory limit (GOMEMLIMIT): 512MiB"
echo "Aggressive GC enabled (GOGC=50)"

View file

@ -0,0 +1,62 @@
#!/bin/bash
set -e
IMAGE_NAME="localhost/gopherbook-builder:latest"
CONTAINER_NAME="gopherbook-builder-tmp"
OUTPUT_DIR="./binaries"
echo "=== Building cross-compilation container ==="
podman build --force-rm -t "$IMAGE_NAME" -f Containerfile.build .
if [ $? -ne 0 ]; then
echo "Image build failed. Exiting."
exit 1
fi
echo ""
echo "=== Creating temporary container ==="
podman create --name "$CONTAINER_NAME" "$IMAGE_NAME"
echo ""
echo "=== Creating output directory ==="
mkdir -p "$OUTPUT_DIR"
rm -f "$OUTPUT_DIR"/*
echo ""
echo "=== Extracting Linux binary ==="
podman cp "$CONTAINER_NAME:/app/bin/gopherbook-linux" "$OUTPUT_DIR/gopherbook-linux"
chmod +x "$OUTPUT_DIR/gopherbook-linux"
echo ""
echo "=== Extracting Windows binary ==="
podman cp "$CONTAINER_NAME:/app/bin/gopherbook-windows.exe" "$OUTPUT_DIR/gopherbook-windows.exe"
chmod +x "$OUTPUT_DIR/gopherbook-windows.exe"
echo ""
echo "=== Cleaning up temporary container ==="
podman rm "$CONTAINER_NAME"
echo ""
echo "=== Build complete! ==="
echo "Binaries are in: $OUTPUT_DIR/"
ls -lh "$OUTPUT_DIR/"
echo ""
echo "=== Binary sizes ==="
du -h "$OUTPUT_DIR"/*
echo ""
echo "=== Cleaning up builder image ==="
podman rmi "$IMAGE_NAME"
echo ""
echo "✓ Done! Your binaries are ready:"
echo " • Linux: $OUTPUT_DIR/gopherbook-linux"
echo " • Windows: $OUTPUT_DIR/gopherbook-windows.exe"
echo ""
echo "To run the Linux binary:"
echo " $OUTPUT_DIR/gopherbook-linux"
echo ""
echo "To test the Windows binary (requires wine):"
echo " wine $OUTPUT_DIR/gopherbook-windows.exe"

View file

@ -0,0 +1,161 @@
#!/bin/bash
set -e
VERSION="${1:-v1.3.000}"
RELEASE_DIR="./releases"
BINARIES_DIR="./binaries"
echo "=== Packaging Gopherbook $VERSION ==="
echo ""
# Create release directory
mkdir -p "$RELEASE_DIR"
# Clean old releases for this version
rm -f "$RELEASE_DIR"/gopherbook-$VERSION-*
# Check if binaries exist
if [ ! -f "$BINARIES_DIR/gopherbook-linux" ] || [ ! -f "$BINARIES_DIR/gopherbook-windows.exe" ]; then
echo "Error: Binaries not found. Run ./build-and-extract.sh first."
exit 1
fi
echo "=== Creating README.txt ==="
cat > /tmp/README.txt << 'EOF'
Gopherbook - Comic Book Reader (CBZ/CBT)
=========================================
Quick Start:
------------
1. Run the gopherbook executable
2. Open your browser to http://localhost:8080
3. Register a new user account
4. Upload your CBZ/CBT comic files
Features:
---------
• Supports CBZ (ZIP) and CBT (TAR) formats
• Password-protected archives
• Tag management and filtering
• Bookmark pages
• Auto-organize by artist and story arc
• Watch folder for automatic imports
• Multi-user support with admin controls
Watch Folder:
-------------
Place CBZ/CBT files in the ./watch/<username>/ directory
and they will be automatically imported to your library.
Directory Structure:
--------------------
./library/ - Your comic library (per user)
./cache/ - Cover image cache
./etc/ - User data and settings
./watch/ - Watch folders for auto-import
Default Port: 8080
For more information, visit:
https://github.com/riomoo/gopherbook
https://codeberg.org/riomoo/gofudge
https://gitgud.io/riomoo/gopherbook
EOF
echo "=== Creating Linux package ==="
LINUX_DIR="/tmp/gopherbook-linux"
rm -rf "$LINUX_DIR"
mkdir -p "$LINUX_DIR"
# Copy Linux binary
cp "$BINARIES_DIR/gopherbook-linux" "$LINUX_DIR/gopherbook"
chmod +x "$LINUX_DIR/gopherbook"
# Copy documentation
cp /tmp/README.txt "$LINUX_DIR/"
# Create run script
cat > "$LINUX_DIR/run.sh" << 'EOF'
#!/bin/bash
echo "Starting Gopherbook..."
echo "Open your browser to: http://localhost:8080"
echo "Press Ctrl+C to stop"
echo ""
./gopherbook
EOF
chmod +x "$LINUX_DIR/run.sh"
# Package Linux
cd /tmp
tar -czf "$LINUX_DIR.tar.gz" gopherbook-linux/
cd - > /dev/null
mv "/tmp/gopherbook-linux.tar.gz" "$RELEASE_DIR/gopherbook-$VERSION-linux-amd64.tar.gz"
rm -rf "$LINUX_DIR"
echo "✓ Linux package created"
echo ""
echo "=== Creating Windows package ==="
WINDOWS_DIR="/tmp/gopherbook-windows"
rm -rf "$WINDOWS_DIR"
mkdir -p "$WINDOWS_DIR"
# Copy Windows binary
cp "$BINARIES_DIR/gopherbook-windows.exe" "$WINDOWS_DIR/gopherbook.exe"
# Copy documentation (Windows line endings)
unix2dos < /tmp/README.txt > "$WINDOWS_DIR/README.txt" 2>/dev/null || cp /tmp/README.txt "$WINDOWS_DIR/README.txt"
# Create batch file
cat > "$WINDOWS_DIR/run.bat" << 'EOF'
@echo off
echo Starting Gopherbook...
echo Open your browser to: http://localhost:8080
echo Press Ctrl+C to stop
echo.
gopherbook.exe
pause
EOF
# Create PowerShell script
cat > "$WINDOWS_DIR/run.ps1" << 'EOF'
Write-Host "Starting Gopherbook..." -ForegroundColor Green
Write-Host "Open your browser to: http://localhost:8080" -ForegroundColor Cyan
Write-Host "Press Ctrl+C to stop" -ForegroundColor Yellow
Write-Host ""
.\gopherbook.exe
EOF
# Package Windows
cd /tmp
zip -q -r "$WINDOWS_DIR.zip" gopherbook-windows/
cd - > /dev/null
mv "/tmp/gopherbook-windows.zip" "$RELEASE_DIR/gopherbook-$VERSION-windows-amd64.zip"
rm -rf "$WINDOWS_DIR"
echo "✓ Windows package created"
echo ""
echo "=== Creating checksums ==="
cd "$RELEASE_DIR"
sha256sum gopherbook-$VERSION-*.tar.gz gopherbook-$VERSION-*.zip > gopherbook-$VERSION-checksums.txt
cd - > /dev/null
echo "✓ Checksums created"
echo ""
echo "=== Release packages ready! ==="
echo ""
ls -lh "$RELEASE_DIR"/gopherbook-$VERSION-*
echo ""
echo "Release files:"
echo "$RELEASE_DIR/gopherbook-$VERSION-linux-amd64.tar.gz"
echo "$RELEASE_DIR/gopherbook-$VERSION-windows-amd64.zip"
echo "$RELEASE_DIR/gopherbook-$VERSION-checksums.txt"
echo ""
echo "Upload these files to GitHub Releases!"
# Cleanup
rm -f /tmp/README.txt

View file

@ -0,0 +1,46 @@
#!/bin/bash
IMAGE_NAME="localhost/gopherbook:latest"
CONTAINER_NAME="gopherbook"
echo "Building new image: $IMAGE_NAME..."
docker build --force-rm -t "$IMAGE_NAME" .
if [ $? -ne 0 ]; then
echo "Image build failed. Exiting script."
exit 1
fi
# Ensure directories exist with correct permissions
mkdir -p ./library ./cache ./etc ./watch
if docker container exists "$CONTAINER_NAME"; then
echo "Container '$CONTAINER_NAME' already exists. Stopping and removing it..."
docker stop "$CONTAINER_NAME"
docker rm "$CONTAINER_NAME"
fi
echo "Starting new container from image: $IMAGE_NAME..."
# IMPROVED: Better memory settings and limits
docker run -d --name "$CONTAINER_NAME" \
--memory=512m \
--restart unless-stopped \
-p 12010:8080 \
-v ./library:/app/library \
-v ./cache:/app/cache \
-v ./etc:/app/etc \
-v ./watch:/app/watch \
"$IMAGE_NAME"
if [ $? -ne 0 ]; then
echo "Failed to start new container. Exiting script."
exit 1
fi
echo "Cleaning up old images..."
docker image prune --force
echo "Update and cleanup complete!"
echo "Container is running with memory limit: 512MB"
echo "Go memory limit (GOMEMLIMIT): 512MiB"
echo "Aggressive GC enabled (GOGC=50)"

View file

@ -41,6 +41,6 @@ echo "Cleaning up old images..."
podman image prune --force
echo "Update and cleanup complete!"
echo "Container is running with memory limit: 512MB, swap: 512MB"
echo "Container is running with memory limit: 512MB"
echo "Go memory limit (GOMEMLIMIT): 512MiB"
echo "Aggressive GC enabled (GOGC=50)"