Merge pull request 'merge: planned additions' (#5) from dev/finalized-features into main
Reviewed-on: #5
This commit is contained in:
commit
44b2874780
12 changed files with 1427 additions and 189 deletions
|
|
@ -9,3 +9,4 @@ xml-template
|
||||||
cache
|
cache
|
||||||
etc
|
etc
|
||||||
library
|
library
|
||||||
|
watch
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@
|
||||||
cache
|
cache
|
||||||
library
|
library
|
||||||
etc
|
etc
|
||||||
|
watch
|
||||||
|
|
|
||||||
5
Makefile
Normal file
5
Makefile
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
build:
|
||||||
|
go build -o bin/main app/gopherbook/main.go
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf watch etc library cache
|
||||||
55
README.md
55
README.md
|
|
@ -1,7 +1,7 @@
|
||||||
# Gopherbook – Self-Hosted Comic Library & CBZ Reader
|
# Gopherbook – Self-Hosted Comic Library & CBZ/CBT Reader
|
||||||
|
|
||||||
Gopherbook is a lightweight, single-binary, self-hosted web comic reader and library manager written in Go.
|
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 files), including support for password-protected/encrypted archives, per-user libraries, tagging, automatic organization, and a clean modern reader.
|
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.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
@ -9,10 +9,11 @@ It is designed for people who want full control over their digital comic collect
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Upload & read `.cbz` (ZIP-based) comics directly in the browser
|
- Upload & read `.cbz` (ZIP-based) or `.cbt` (TAR-based) comics directly in the browser
|
||||||
|
- **Watch folder support for bulk imports** – drop CBZ/CBT files into your watch folder and they're automatically imported
|
||||||
- Supports 8 Megapixel images at 512MB memory limits
|
- Supports 8 Megapixel images at 512MB memory limits
|
||||||
- (increase in bash script for higher Megapixels or remove the limitation if you don't care)
|
- (increase in bash script for higher Megapixels or remove the limitation if you don't care)
|
||||||
- Full support for password-protected/encrypted CBZ files (AES-256 via yeka/zip)
|
- Full support for password-protected/encrypted CBZ files (AES-256 via yeka/zip) or CBT files (AES-256-CFB Openssl)
|
||||||
- Automatically tries all previously successful passwords when opening a new encrypted comic
|
- Automatically tries all previously successful passwords when opening a new encrypted comic
|
||||||
- Persists discovered passwords securely (AES-encrypted on disk, key derived from your login password)
|
- Persists discovered passwords securely (AES-encrypted on disk, key derived from your login password)
|
||||||
- Extracts ComicInfo.xml metadata (title, series, number, writer, inker, tags, story arc, etc.)
|
- Extracts ComicInfo.xml metadata (title, series, number, writer, inker, tags, story arc, etc.)
|
||||||
|
|
@ -42,7 +43,6 @@ It is designed for people who want full control over their digital comic collect
|
||||||
- Or just download a pre-built binary from Releases (when available)
|
- Or just download a pre-built binary from Releases (when available)
|
||||||
|
|
||||||
### Quick start (from source)
|
### Quick start (from source)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://codeberg.org/riomoo/gopherbook.git
|
git clone https://codeberg.org/riomoo/gopherbook.git
|
||||||
cd gopherbook
|
cd gopherbook
|
||||||
|
|
@ -53,11 +53,10 @@ go build -o gopherbook app/gopherbook/main.go
|
||||||
Then open http://localhost:8080 in your browser.
|
Then open http://localhost:8080 in your browser.
|
||||||
|
|
||||||
## If you want to use this with podman:
|
## If you want to use this with podman:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://codeberg.org/riomoo/gopherbook.git
|
git clone https://codeberg.org/riomoo/gopherbook.git
|
||||||
cd gopherbook
|
cd gopherbook
|
||||||
./bash-scripts/run.sh
|
./scripts-bash/run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open http://localhost:12010 in your browser.
|
Then open http://localhost:12010 in your browser.
|
||||||
|
|
@ -65,23 +64,56 @@ Then open http://localhost:12010 in your browser.
|
||||||
### First launch
|
### First launch
|
||||||
1. On first run there are no users → registration is open
|
1. On first run there are no users → registration is open
|
||||||
2. Create the first account → this user automatically becomes admin
|
2. Create the first account → this user automatically becomes admin
|
||||||
3. Log in → start uploading CBZ files
|
3. Log in → start uploading CBZ/CBT files
|
||||||
|
|
||||||
## Directory layout after first login
|
## Directory layout after first login
|
||||||
|
|
||||||
```
|
```
|
||||||
./library/username/ ← your comics (organized or Unorganized/)
|
./library/username/ ← your comics (organized or Unorganized/)
|
||||||
./library/username/comics.json ← metadata index
|
./library/username/comics.json ← metadata index
|
||||||
./library/username/tags.json ← tag definitions & counts
|
./library/username/tags.json ← tag definitions & counts
|
||||||
./library/username/passwords.json ← encrypted password vault (AES)
|
./library/username/passwords.json ← encrypted password vault (AES)
|
||||||
./cache/covers/username/ ← generated cover thumbnails
|
./cache/covers/username/ ← generated cover thumbnails
|
||||||
|
./watch/username/ ← watch folder for bulk imports (auto-scanned)
|
||||||
./etc/users.json ← user accounts (bcrypt hashes)
|
./etc/users.json ← user accounts (bcrypt hashes)
|
||||||
./etc/admin.json ← admin settings (registration toggle)
|
./etc/admin.json ← admin settings (registration toggle)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Watch folder for bulk imports
|
||||||
|
|
||||||
|
Gopherbook includes an automatic watch folder system that makes bulk importing comics effortless:
|
||||||
|
|
||||||
|
- **Per-user watch folders**: Each user gets their own watch folder at `./watch/[username]/`
|
||||||
|
- **Automatic scanning**: The system checks for new CBZ/CBT files every 10 seconds
|
||||||
|
- **Smart debouncing**: Waits 5 seconds after detecting files to ensure they're fully copied
|
||||||
|
- **File validation**: Checks that files aren't still being written before importing
|
||||||
|
- **Duplicate handling**: Automatically renames files if they already exist (adds _1, _2, etc.)
|
||||||
|
- **Zero configuration**: Just drop CBZ/CBT files into your watch folder and they appear in your library
|
||||||
|
|
||||||
|
### How to use
|
||||||
|
|
||||||
|
1. After logging in, your personal watch folder is at `./watch/[yourusername]/`
|
||||||
|
2. Copy or move CBZ/CBT files into this folder using any method:
|
||||||
|
- Direct file copy/paste
|
||||||
|
- SCP/SFTP upload
|
||||||
|
- Network share mount
|
||||||
|
- Automated scripts
|
||||||
|
3. Within ~15 seconds, files are automatically imported to your library
|
||||||
|
4. Files are **moved** (not copied) to preserve disk space
|
||||||
|
5. Check the API endpoint `/api/watch-folder` to see pending files
|
||||||
|
|
||||||
|
**Example workflow:**
|
||||||
|
```bash
|
||||||
|
# Bulk copy comics to your watch folder
|
||||||
|
cp ~/Downloads/*.cbz ./watch/myusername/
|
||||||
|
cp ~/Downloads/*.cbt ./watch/myusername/
|
||||||
|
|
||||||
|
# Wait ~15 seconds, then check your library in the web UI
|
||||||
|
# All comics will be imported and organized automatically
|
||||||
|
```
|
||||||
|
|
||||||
## How encrypted/password-protected comics work
|
## How encrypted/password-protected comics work
|
||||||
|
|
||||||
- When you upload or scan an encrypted CBZ that has no known password yet, the server marks it as Encrypted = true.
|
- When you upload or scan an encrypted CBZ/CBT that has no known password yet, the server marks it as Encrypted = true.
|
||||||
- The first time you open it in the reader, a password prompt appears.
|
- The first time you open it in the reader, a password prompt appears.
|
||||||
- If the password is correct, Gopherbook:
|
- If the password is correct, Gopherbook:
|
||||||
- Stores the password (encrypted with a key derived from your login password)
|
- Stores the password (encrypted with a key derived from your login password)
|
||||||
|
|
@ -152,7 +184,7 @@ Please open an issue first for bigger changes.
|
||||||
- Everyone who hoards comics ❤️
|
- Everyone who hoards comics ❤️
|
||||||
|
|
||||||
Enjoy your library!
|
Enjoy your library!
|
||||||
— Happy reading with Gopherbook
|
– Happy reading with Gopherbook
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
|
@ -160,6 +192,7 @@ Enjoy your library!
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -772,8 +772,8 @@
|
||||||
<div class="upload-section">
|
<div class="upload-section">
|
||||||
<h2 style="margin-bottom: 16px; color: #c9d1d9;">Upload Comic</h2>
|
<h2 style="margin-bottom: 16px; color: #c9d1d9;">Upload Comic</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Select File (CBZ)</label>
|
<label>Select File (CBZ or CBT)</label>
|
||||||
<input type="file" id="fileUpload" accept=".cbz">
|
<input type="file" id="fileUpload" accept=".cbz,.cbt">
|
||||||
</div>
|
</div>
|
||||||
<button onclick="uploadComic()">Upload</button>
|
<button onclick="uploadComic()">Upload</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
204
scripts-bash/cbt.sh
Executable file
204
scripts-bash/cbt.sh
Executable file
|
|
@ -0,0 +1,204 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# CBT (Comic Book Tar) Creator and Extractor
|
||||||
|
# Creates and extracts .cbt files with optional AES encryption
|
||||||
|
# Compatible with Go server encryption format
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
show_usage() {
|
||||||
|
echo "Usage:"
|
||||||
|
echo " Create: $0 create -i <input_directory> -o <output_file> [-p <password>]"
|
||||||
|
echo " Extract: $0 extract -i <input_file> -o <output_directory> [-p <password>]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " -i Input directory/file"
|
||||||
|
echo " -o Output file/directory"
|
||||||
|
echo " -p Password for encryption/decryption (optional)"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 create -i ./comics -o mycomic.cbt"
|
||||||
|
echo " $0 create -i ./comics -o mycomic.cbt -p mysecretpass"
|
||||||
|
echo " $0 extract -i mycomic.cbt -o ./extracted"
|
||||||
|
echo " $0 extract -i mycomic.cbt -o ./extracted -p mysecretpass"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
MODE="$1"
|
||||||
|
shift || show_usage
|
||||||
|
|
||||||
|
INPUT=""
|
||||||
|
OUTPUT=""
|
||||||
|
PASSWORD=""
|
||||||
|
|
||||||
|
while getopts "i:o:p:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
i) INPUT="$OPTARG" ;;
|
||||||
|
o) OUTPUT="$OPTARG" ;;
|
||||||
|
p) PASSWORD="$OPTARG" ;;
|
||||||
|
h) show_usage ;;
|
||||||
|
*) show_usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate mode
|
||||||
|
if [ "$MODE" != "create" ] && [ "$MODE" != "extract" ]; then
|
||||||
|
echo "Error: First argument must be 'create' or 'extract'"
|
||||||
|
show_usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate required arguments
|
||||||
|
if [ -z "$INPUT" ] || [ -z "$OUTPUT" ]; then
|
||||||
|
echo "Error: Input and output are required"
|
||||||
|
show_usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Encrypt data using AES-CFB
|
||||||
|
encrypt_data() {
|
||||||
|
local input_file="$1"
|
||||||
|
local output_file="$2"
|
||||||
|
local password="$3"
|
||||||
|
|
||||||
|
# Derive key using SHA256 (same as Go)
|
||||||
|
local key=$(echo -n "$password" | sha256sum | cut -d' ' -f1 | xxd -r -p | base64)
|
||||||
|
|
||||||
|
# Generate random IV (16 bytes for AES)
|
||||||
|
local iv=$(openssl rand -base64 16)
|
||||||
|
|
||||||
|
# Encrypt using AES-256-CFB
|
||||||
|
# First write IV, then encrypted data
|
||||||
|
echo -n "$iv" | base64 -d > "$output_file"
|
||||||
|
openssl enc -aes-256-cfb -K "$(echo -n "$password" | sha256sum | cut -d' ' -f1)" -iv "$(echo -n "$iv" | base64 -d | xxd -p -c 256)" -in "$input_file" >> "$output_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Decrypt data using AES-CFB (compatible with Go implementation)
|
||||||
|
decrypt_data() {
|
||||||
|
local input_file="$1"
|
||||||
|
local output_file="$2"
|
||||||
|
local password="$3"
|
||||||
|
|
||||||
|
# Extract IV (first 16 bytes)
|
||||||
|
local iv_hex=$(head -c 16 "$input_file" | xxd -p -c 256)
|
||||||
|
|
||||||
|
# Extract ciphertext (rest of file)
|
||||||
|
tail -c +17 "$input_file" > "${output_file}.tmp"
|
||||||
|
|
||||||
|
# Decrypt using AES-256-CFB
|
||||||
|
if ! openssl enc -d -aes-256-cfb -K "$(echo -n "$password" | sha256sum | cut -d' ' -f1)" -iv "$iv_hex" -in "${output_file}.tmp" -out "$output_file" 2>/dev/null; then
|
||||||
|
rm -f "${output_file}.tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "${output_file}.tmp"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# CREATE MODE
|
||||||
|
create_cbt() {
|
||||||
|
INPUT_DIR="$INPUT"
|
||||||
|
OUTPUT_FILE="$OUTPUT"
|
||||||
|
|
||||||
|
# Check if input directory exists
|
||||||
|
if [ ! -d "$INPUT_DIR" ]; then
|
||||||
|
echo "Error: Input directory '$INPUT_DIR' does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure output has .cbt extension
|
||||||
|
if [[ ! "$OUTPUT_FILE" =~ \.cbt$ ]]; then
|
||||||
|
OUTPUT_FILE="${OUTPUT_FILE}.cbt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create unencrypted tar archive
|
||||||
|
echo "Creating tar archive from $INPUT_DIR..."
|
||||||
|
TMP_TAR=$(mktemp)
|
||||||
|
|
||||||
|
# Create tar file, preserving relative paths
|
||||||
|
tar -cf "$TMP_TAR" -C "$INPUT_DIR" .
|
||||||
|
|
||||||
|
# List files that were added
|
||||||
|
echo ""
|
||||||
|
echo "Files added:"
|
||||||
|
tar -tf "$TMP_TAR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -n "$PASSWORD" ]; then
|
||||||
|
# Encrypted mode
|
||||||
|
echo "Encrypting with AES-CFB (Go-compatible format)..."
|
||||||
|
|
||||||
|
encrypt_data "$TMP_TAR" "$OUTPUT_FILE" "$PASSWORD"
|
||||||
|
|
||||||
|
rm "$TMP_TAR"
|
||||||
|
echo "Created encrypted CBT: $OUTPUT_FILE"
|
||||||
|
else
|
||||||
|
# Unencrypted mode - just move the tar file
|
||||||
|
mv "$TMP_TAR" "$OUTPUT_FILE"
|
||||||
|
echo "Created unencrypted CBT: $OUTPUT_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# EXTRACT MODE
|
||||||
|
extract_cbt() {
|
||||||
|
INPUT_FILE="$INPUT"
|
||||||
|
OUTPUT_DIR="$OUTPUT"
|
||||||
|
|
||||||
|
# Check if input file exists
|
||||||
|
if [ ! -f "$INPUT_FILE" ]; then
|
||||||
|
echo "Error: Input file '$INPUT_FILE' does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create output directory if it doesn't exist
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
TMP_TAR=$(mktemp)
|
||||||
|
|
||||||
|
# Check if file is encrypted by trying to read as tar first
|
||||||
|
if tar -tf "$INPUT_FILE" >/dev/null 2>&1; then
|
||||||
|
# File is unencrypted tar
|
||||||
|
cp "$INPUT_FILE" "$TMP_TAR"
|
||||||
|
else
|
||||||
|
# File appears to be encrypted
|
||||||
|
if [ -z "$PASSWORD" ]; then
|
||||||
|
echo "Error: File appears to be encrypted. Please provide password with -p flag."
|
||||||
|
rm "$TMP_TAR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Decrypting $INPUT_FILE..."
|
||||||
|
|
||||||
|
if ! decrypt_data "$INPUT_FILE" "$TMP_TAR" "$PASSWORD"; then
|
||||||
|
echo "Error: Failed to decrypt. Wrong password or corrupted file."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Decryption successful!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract tar archive
|
||||||
|
echo "Extracting to $OUTPUT_DIR..."
|
||||||
|
|
||||||
|
if tar -xf "$TMP_TAR" -C "$OUTPUT_DIR" 2>/dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo "Files extracted:"
|
||||||
|
tar -tf "$TMP_TAR"
|
||||||
|
echo ""
|
||||||
|
echo "Extraction complete!"
|
||||||
|
else
|
||||||
|
rm "$TMP_TAR"
|
||||||
|
echo "Error: Failed to extract. File may be corrupted."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm "$TMP_TAR"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$MODE" = "create" ]; then
|
||||||
|
create_cbt
|
||||||
|
elif [ "$MODE" = "extract" ]; then
|
||||||
|
extract_cbt
|
||||||
|
fi
|
||||||
|
|
@ -12,7 +12,7 @@ if [ $? -ne 0 ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure directories exist with correct permissions
|
# Ensure directories exist with correct permissions
|
||||||
mkdir -p ./library ./cache ./etc
|
mkdir -p ./library ./cache ./etc ./watch
|
||||||
|
|
||||||
if podman container exists "$CONTAINER_NAME"; then
|
if podman container exists "$CONTAINER_NAME"; then
|
||||||
echo "Container '$CONTAINER_NAME' already exists. Stopping and removing it..."
|
echo "Container '$CONTAINER_NAME' already exists. Stopping and removing it..."
|
||||||
|
|
@ -29,6 +29,7 @@ podman run -d --name "$CONTAINER_NAME" \
|
||||||
-v ./library:/app/library \
|
-v ./library:/app/library \
|
||||||
-v ./cache:/app/cache \
|
-v ./cache:/app/cache \
|
||||||
-v ./etc:/app/etc \
|
-v ./etc:/app/etc \
|
||||||
|
-v ./watch:/app/watch \
|
||||||
"$IMAGE_NAME"
|
"$IMAGE_NAME"
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
3
scripts-bash/test-cbt.sh
Executable file
3
scripts-bash/test-cbt.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
user="$(gpg2 -qd /home/moo/.local/pdf-keys/userkey)"
|
||||||
|
mkdir -p out
|
||||||
|
./cbt.sh create -i $1 -o ./out/$1.cbt -p $user
|
||||||
231
scripts-go/cbt.go
Normal file
231
scripts-go/cbt.go
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
inputDir := flag.String("input", "", "Input directory containing comic files")
|
||||||
|
outputFile := flag.String("output", "", "Output .cbt file")
|
||||||
|
password := flag.String("password", "", "Password for encryption (leave empty for no encryption)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *inputDir == "" || *outputFile == "" {
|
||||||
|
fmt.Println("Usage: cbt-creator -input <directory> -output <file.cbt> [-password <password>]")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure output has .cbt extension
|
||||||
|
if !strings.HasSuffix(strings.ToLower(*outputFile), ".cbt") {
|
||||||
|
*outputFile += ".cbt"
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if *password != "" {
|
||||||
|
err = createEncryptedCBT(*inputDir, *outputFile, *password)
|
||||||
|
fmt.Printf("Created encrypted CBT: %s\n", *outputFile)
|
||||||
|
} else {
|
||||||
|
err = createUnencryptedCBT(*inputDir, *outputFile)
|
||||||
|
fmt.Printf("Created unencrypted CBT: %s\n", *outputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUnencryptedCBT(inputDir, outputPath string) error {
|
||||||
|
outFile, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
tw := tar.NewWriter(outFile)
|
||||||
|
defer tw.Close()
|
||||||
|
|
||||||
|
// Collect all files
|
||||||
|
var files []string
|
||||||
|
err = filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add files to tar
|
||||||
|
for _, file := range files {
|
||||||
|
relPath, err := filepath.Rel(inputDir, file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file
|
||||||
|
data, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write tar header
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: relPath,
|
||||||
|
Mode: 0600,
|
||||||
|
Size: int64(len(data)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file data
|
||||||
|
if _, err := tw.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Added: %s\n", relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEncryptedCBT(inputDir, outputPath, password string) error {
|
||||||
|
// Create temporary unencrypted tar
|
||||||
|
tmpTar := outputPath + ".tmp"
|
||||||
|
tmpFile, err := os.Create(tmpTar)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tw := tar.NewWriter(tmpFile)
|
||||||
|
|
||||||
|
// Collect all files
|
||||||
|
var files []string
|
||||||
|
err = filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tmpTar)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add files to tar
|
||||||
|
for _, file := range files {
|
||||||
|
relPath, err := filepath.Rel(inputDir, file)
|
||||||
|
if err != nil {
|
||||||
|
tw.Close()
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tmpTar)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file
|
||||||
|
data, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
tw.Close()
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tmpTar)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write tar header
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: relPath,
|
||||||
|
Mode: 0600,
|
||||||
|
Size: int64(len(data)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
tw.Close()
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tmpTar)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file data
|
||||||
|
if _, err := tw.Write(data); err != nil {
|
||||||
|
tw.Close()
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tmpTar)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Added: %s\n", relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
tw.Close()
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
// Read the tar file
|
||||||
|
tarData, err := os.ReadFile(tmpTar)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmpTar)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Encrypting...")
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
key := deriveKey(password)
|
||||||
|
encrypted, err := encryptAES(tarData, key)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmpTar)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write encrypted file
|
||||||
|
err = os.WriteFile(outputPath, encrypted, 0644)
|
||||||
|
os.Remove(tmpTar)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveKey(password string) []byte {
|
||||||
|
hash := sha256.Sum256([]byte(password))
|
||||||
|
return hash[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptAES(plaintext []byte, key []byte) ([]byte, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate IV
|
||||||
|
iv := make([]byte, aes.BlockSize)
|
||||||
|
if _, err := rand.Read(iv); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stream := cipher.NewCFBEncrypter(block, iv)
|
||||||
|
|
||||||
|
ciphertext := make([]byte, len(plaintext))
|
||||||
|
stream.XORKeyStream(ciphertext, plaintext)
|
||||||
|
|
||||||
|
// Prepend IV to ciphertext
|
||||||
|
return append(iv, ciphertext...), nil
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue