Skip to content

PsychQuant/GiftHub

Repository files navigation

GiftHub

Self-hosted Git LFS server with pluggable storage backends.

GiftHub implements the Git LFS Batch API, so any standard git lfs client can push and pull large files through it. The actual bytes are stored on a backend of your choice — local disk, Google Drive, or Google Drive via the gws CLI.

Why

Git repos should stay lightweight. Videos, datasets, and other large files don't belong in git history. GiftHub lets you git lfs track them and store the real bytes somewhere else — your own disk, your Google Drive, or (eventually) S3.

How it works

git repo                    GiftHub server              Storage backend
─────────                   ──────────────              ───────────────
*.mp4 (pointer file)  ───►  POST /objects/batch   ───►  Google Drive
  ~130 bytes                PUT  /objects/:oid          (or local disk)
                            GET  /objects/:oid          (or gws proxy)

When you git push, the LFS client sends the large file to GiftHub. GiftHub stores it on the configured backend using the file's SHA-256 hash as the key (content-addressed). Your git repo only ever contains a tiny pointer file.

When you git clone + git lfs pull, the LFS client asks GiftHub for the file by its hash, and GiftHub streams it back from the backend.

CLI Reference

The CLI binary is gfh (GiftHub).

gfh serve — Start the LFS server

# Local disk backend (default)
gfh serve
gfh serve --driver local --storage-path ./lfs-objects

# Google Drive via gws CLI (recommended)
gfh serve --driver gwsProxy --drive-folder-id "FOLDER_ID"

# Google Drive via OAuth (programmatic access)
gfh serve --driver googleDrive \
  --drive-folder-id "FOLDER_ID" \
  --drive-credentials ~/gws-creds.json

# Custom host/port
gfh serve --hostname 0.0.0.0 --port 9080
Option Default Description
--driver local Storage backend: local, gwsProxy, googleDrive
--hostname / -h 127.0.0.1 Hostname to bind to
--port / -p 8080 Port to listen on
--storage-path ./lfs-objects Root directory (local driver)
--drive-folder-id Google Drive folder ID (required for Drive drivers)
--drive-credentials Path to gws auth export JSON (googleDrive driver)
--registry-path ./lfs-registry.json Path to object registry file

gfh alias — Map OIDs to existing Drive files

When you already have files on Google Drive and want Git LFS to serve them without re-uploading:

# Register: OID → Drive file ID
gfh alias add <oid> <drive-file-id>

# List all aliases
gfh alias list

# Remove an alias
gfh alias remove <oid>
Option Default Description
--registry-path ./lfs-registry-aliases.json Path to alias registry file

Workflow: Link existing Drive videos to LFS

# 1. Compute SHA-256 of the Drive file (streaming, no download)
gws drive files get --params '{"fileId":"FILE_ID","alt":"media"}' -o /dev/stdout | shasum -a 256

# 2. Register the alias
gfh alias add "SHA256_HASH" "DRIVE_FILE_ID"

# 3. Get the file size
gws drive files get --params '{"fileId":"FILE_ID","fields":"size"}'

# 4. Create the LFS pointer file in your repo
cat > path/to/video.mp4 << EOF
version https://git-lfs.github.com/spec/v1
oid sha256:SHA256_HASH
size FILE_SIZE
EOF

# 5. git add + commit — only the pointer file (130 bytes) is stored
git add path/to/video.mp4
git commit -m "add video via LFS alias"

gfh init — Initialize a GiftHub project

gfh init --drive-folder-id "YOUR_FOLDER_ID"

Creates .gfh.json and stores Drive OAuth credentials in macOS Keychain.

gfh link — Link existing Drive files as LFS pointers

# Link a Drive file to a local path (creates pointer + alias)
gfh link <drive-file-id-or-url> <local-path>

Creates the pointer file locally and registers the OID → Drive file ID alias. Also renames the Drive file to match the local filename.

After linking, run git add <local-path> before gfh pull.

gfh pull — Download LFS objects from Drive

# Pull specific files
gfh pull lectures/04/video.mp4

# Pull all LFS files
gfh pull

Starts a temporary LFS server, then delegates to git lfs pull. Validates that files are tracked by git before pulling, and verifies pointer files are replaced with real content afterward.

gfh rename — Sync Drive filenames to local names

# Rename all pointer files' Drive counterparts
gfh rename

# Rename specific files
gfh rename lectures/04/video.mp4

# Preview without changes
gfh rename --dry-run

gfh upload — Upload files to Drive + create LFS pointers

# Upload and replace local file with pointer
gfh upload path/to/video.mp4

# Upload multiple files
gfh upload *.mp4

# Preview without changes
gfh upload --dry-run path/to/video.mp4

# Keep local file after upload
gfh upload --keep-local path/to/video.mp4

gfh dehydrate — Replace local files with LFS pointers

For files already on Drive. Reclaims local disk space.

# Dehydrate all LFS files in the repo
gfh dehydrate

# Dehydrate specific files
gfh dehydrate lectures/*.mp4

# Preview without changes
gfh dehydrate --dry-run

To restore files: gfh pull <file> or git lfs pull.

gfh version — Print version

gfh version

Storage backends

Backend Driver Auth Best for
Local disk local None Development, small teams
Google Drive drive OAuth via macOS Keychain Personal use, programmatic access

Legacy driver values gwsProxy and googleDrive auto-map to drive.

Local disk

Objects stored as files at <root>/<oid[0:2]>/<oid[2:4]>/<oid>.

gfh serve --driver local --storage-path ./lfs-objects

Google Drive via gws CLI (recommended for personal use)

Delegates all Drive operations to the gws CLI. No tokens to manage — just gws auth login once.

# 1. Install and authenticate gws
npm install -g @googleworkspace/cli
gws auth setup
gws auth login

# 2. Create a folder on Drive for LFS objects
gws drive files create --json '{"name":"GiftHub-LFS","mimeType":"application/vnd.google-apps.folder"}'
# Note the folder ID from the response

# 3. Start the server
gfh serve --driver gwsProxy --drive-folder-id "YOUR_FOLDER_ID"

Google Drive via OAuth (for programmatic access)

Uses the Drive REST API directly with a refresh token.

gws auth export > ~/gws-creds.json
# Edit the file to fill in unmasked values

gfh serve --driver googleDrive \
  --drive-folder-id "YOUR_FOLDER_ID" \
  --drive-credentials ~/gws-creds.json

Git LFS setup

Point your repo's LFS to the GiftHub server:

# In your git repo
git lfs install
git lfs track "*.mp4" "*.mov" "*.mkv" "*.zip"
git config lfs.url http://localhost:8080/owner/repo.git/info/lfs

git add .gitattributes
git add large-file.mp4   # git stores a ~130 byte pointer
git commit -m "add video via LFS"
git push                  # LFS uploads the real file to GiftHub

Build

Requires Swift 6.0+ and macOS 14+.

swift build
swift test

# Install to ~/bin
swift build --configuration release
mkdir -p ~/bin
cp .build/release/gfh ~/bin/

Important: Make sure ~/bin is in your PATH. Add this to ~/.zshrc or ~/.zprofile:

export PATH="$HOME/bin:$PATH"

Then restart your terminal or run source ~/.zshrc.

Verify: gfh --help

Working with dehydrated files

After gfh dehydrate, local .mp4 files are replaced with ~130 byte LFS pointers. The pointer content looks like:

version https://git-lfs.github.com/spec/v1
oid sha256:00d55d6c88cf...
size 1238181437

Getting the OID from a dehydrated file

Do NOT use git lfs pointer --file — it treats the pointer as a regular file and hashes the 130-byte text, returning a wrong OID.

Correct methods:

# Method 1: git lfs ls-files (always correct, regardless of hydration state)
git lfs ls-files --long | grep "video.mp4"
# 00d55d6c... - video.mp4

# Method 2: Read the pointer content directly
grep 'oid sha256:' video.mp4 | sed 's/oid sha256://'

# Method 3: Check first, then branch
if git lfs pointer --check --file video.mp4 2>/dev/null; then
    # It's a pointer — read OID from content
    OID=$(grep 'oid sha256:' video.mp4 | sed 's/oid sha256://')
else
    # It's a real file — compute OID
    OID=$(git lfs pointer --file video.mp4 | grep sha256 | sed 's/.*sha256://')
fi

Lifecycle

Local file (1.2 GB)
  ──gfh upload──►  Google Drive + local pointer (130 B)
  ──gfh dehydrate──►  local pointer (130 B)  [if already on Drive]
  ──gfh pull──►  Local file (1.2 GB)  [download from Drive]

Architecture

GiftHub/
├── Packages/PawKit/           # Storage layer (drivers + registry)
│   ├── StorageDriver.swift    # Protocol: put/get/head/delete/stream
│   ├── LocalDiskDriver.swift
│   ├── DriveDriver.swift      # Google Drive via Keychain OAuth
│   ├── KeychainHelper.swift   # macOS Keychain read/write
│   ├── AliasRegistry.swift    # OID → Drive file ID mapping
│   ├── ContentHash.swift      # SHA-256 + Dropbox content hash
│   └── StorageConfiguration.swift
├── Sources/GiftHub/           # Core library (Hummingbird HTTP server)
│   ├── LFS/                   # Git LFS Batch API handlers
│   │   └── LFSPointer.swift   # Pointer file detection utility
│   ├── Auth/
│   ├── Registry/              # Object metadata tracking
│   └── Server/
└── Sources/gfs/               # CLI entry point (binary: gfh)
    ├── GFS.swift              # Main command
    ├── Serve.swift            # serve subcommand
    ├── Init.swift             # init subcommand
    ├── Upload.swift           # upload subcommand
    ├── Link.swift             # link subcommand
    ├── Pull.swift             # pull subcommand
    ├── Dehydrate.swift        # dehydrate subcommand
    ├── Alias.swift            # alias subcommand
    ├── Rename.swift           # rename subcommand
    └── Version.swift

About

Self-hosted Git LFS backend — keep your Git workflow, own your large file infrastructure

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors