Compare commits
No commits in common. "gh-pages" and "main" have entirely different histories.
242 changed files with 39565 additions and 14441 deletions
34
.github/workflows/go.yml
vendored
Normal file
34
.github/workflows/go.yml
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# This workflow will build a golang project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
|
||||
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install Task
|
||||
run: go install github.com/go-task/task/v3/cmd/task@latest
|
||||
|
||||
- name: Build
|
||||
run: ~/go/bin/task build
|
||||
|
||||
- name: Test
|
||||
run: ~/go/bin/task test
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
17
.github/workflows/mkdocs.yml
vendored
Normal file
17
.github/workflows/mkdocs.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
name: mkdocs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install mkdocs-material
|
||||
- run: mkdocs gh-deploy --force
|
||||
105
.github/workflows/release.yml
vendored
Normal file
105
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
mkdir -p dist
|
||||
|
||||
# Linux amd64
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o dist/borg-linux-amd64 main.go
|
||||
|
||||
# Linux arm64
|
||||
GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o dist/borg-linux-arm64 main.go
|
||||
|
||||
# macOS amd64
|
||||
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o dist/borg-darwin-amd64 main.go
|
||||
|
||||
# macOS arm64
|
||||
GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o dist/borg-darwin-arm64 main.go
|
||||
|
||||
# Windows amd64
|
||||
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o dist/borg-windows-amd64.exe main.go
|
||||
|
||||
- name: Build WASM module
|
||||
run: |
|
||||
GOOS=js GOARCH=wasm go build -o dist/stmf.wasm ./pkg/wasm/stmf/
|
||||
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" dist/ 2>/dev/null || \
|
||||
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" dist/
|
||||
|
||||
- name: Build Console STIM
|
||||
run: |
|
||||
# Build borg for current platform first
|
||||
go build -o borg main.go
|
||||
|
||||
# Build the encrypted console demo
|
||||
./borg console build -p "borg-demo" -o dist/console.stim -s js/borg-stmf
|
||||
|
||||
- name: Create checksums
|
||||
run: |
|
||||
cd dist
|
||||
sha256sum * > checksums.txt
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Borg ${{ steps.version.outputs.VERSION }}
|
||||
body: |
|
||||
## Borg ${{ steps.version.outputs.VERSION }}
|
||||
|
||||
### Downloads
|
||||
|
||||
| Platform | Binary |
|
||||
|----------|--------|
|
||||
| Linux x64 | `borg-linux-amd64` |
|
||||
| Linux ARM64 | `borg-linux-arm64` |
|
||||
| macOS x64 | `borg-darwin-amd64` |
|
||||
| macOS ARM64 | `borg-darwin-arm64` |
|
||||
| Windows x64 | `borg-windows-amd64.exe` |
|
||||
|
||||
### Console Demo
|
||||
|
||||
The `console.stim` is an encrypted PWA demo. Run it with:
|
||||
```bash
|
||||
borg console serve console.stim --open
|
||||
```
|
||||
Password: `borg-demo`
|
||||
|
||||
### WASM Module
|
||||
|
||||
- `stmf.wasm` - Browser encryption module
|
||||
- `wasm_exec.js` - Go WASM runtime
|
||||
|
||||
files: |
|
||||
dist/borg-linux-amd64
|
||||
dist/borg-linux-arm64
|
||||
dist/borg-darwin-amd64
|
||||
dist/borg-darwin-arm64
|
||||
dist/borg-windows-amd64.exe
|
||||
dist/stmf.wasm
|
||||
dist/wasm_exec.js
|
||||
dist/console.stim
|
||||
dist/checksums.txt
|
||||
draft: false
|
||||
prerelease: false
|
||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
borg
|
||||
*.cube
|
||||
.task
|
||||
*.datanode
|
||||
.idea
|
||||
coverage.txt
|
||||
|
||||
# Demo content (hosted on CDN)
|
||||
demo-track.smsg
|
||||
|
||||
# Dev artifacts
|
||||
.playwright-mcp/
|
||||
80
.goreleaser.yaml
Normal file
80
.goreleaser.yaml
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Goreleaser config for Borg
|
||||
# Non-invasive: builds the existing CLI binary without changing functionality.
|
||||
project_name: borg
|
||||
|
||||
version: 2
|
||||
|
||||
dist: dist
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- id: borg
|
||||
main: ./main.go
|
||||
binary: borg
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
- freebsd
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
mod_timestamp: '{{ .CommitDate }}'
|
||||
|
||||
archives:
|
||||
- id: archive
|
||||
builds:
|
||||
- borg
|
||||
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- LICENSE.md
|
||||
- README.md
|
||||
- docs/**
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
use: github-native
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs: '
|
||||
- '^test: '
|
||||
|
||||
release:
|
||||
# By default goreleaser creates GitHub releases from tags.
|
||||
prerelease: auto
|
||||
mode: replace
|
||||
|
||||
brews:
|
||||
- name: borg
|
||||
repository:
|
||||
owner: Snider
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
homepage: https://github.com/Snider/Borg
|
||||
description: "Borg Data Collector CLI"
|
||||
commit_author:
|
||||
name: goreleaserbot
|
||||
email: bot@goreleaser.com
|
||||
test: |
|
||||
system "#{bin}/borg", "--help"
|
||||
install: |
|
||||
bin.install "borg"
|
||||
576
404.html
576
404.html
|
|
@ -1,576 +0,0 @@
|
|||
|
||||
<!doctype html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<meta name="description" content="CLI and library for collecting repositories, websites, and PWAs into portable data artifacts.">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="icon" href="/assets/images/favicon.png">
|
||||
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.1">
|
||||
|
||||
|
||||
|
||||
<title>Borg Data Collector</title>
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/assets/stylesheets/main.484c7ddc.min.css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/assets/stylesheets/palette.ab4e12ef.min.css">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
|
||||
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
|
||||
|
||||
|
||||
|
||||
<script>__md_scope=new URL("/",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<body dir="ltr" data-md-color-scheme="default" data-md-color-primary="blue" data-md-color-accent="indigo">
|
||||
|
||||
|
||||
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
|
||||
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
|
||||
<label class="md-overlay" for="__drawer"></label>
|
||||
<div data-md-component="skip">
|
||||
|
||||
</div>
|
||||
<div data-md-component="announce">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<header class="md-header" data-md-component="header">
|
||||
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||
<a href="/." title="Borg Data Collector" class="md-header__button md-logo" aria-label="Borg Data Collector" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
|
||||
</a>
|
||||
<label class="md-header__button md-icon" for="__drawer">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||
</label>
|
||||
<div class="md-header__title" data-md-component="header-title">
|
||||
<div class="md-header__ellipsis">
|
||||
<div class="md-header__topic">
|
||||
<span class="md-ellipsis">
|
||||
Borg Data Collector
|
||||
</span>
|
||||
</div>
|
||||
<div class="md-header__topic" data-md-component="header-topic">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form class="md-header__option" data-md-component="palette">
|
||||
|
||||
|
||||
|
||||
|
||||
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="blue" data-md-color-accent="indigo" aria-hidden="true" type="radio" name="__palette" id="__palette_0">
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-header__button md-icon" for="__search">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||
</label>
|
||||
<div class="md-search" data-md-component="search" role="dialog">
|
||||
<label class="md-search__overlay" for="__search"></label>
|
||||
<div class="md-search__inner" role="search">
|
||||
<form class="md-search__form" name="search">
|
||||
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
|
||||
<label class="md-search__icon md-icon" for="__search">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
|
||||
</label>
|
||||
<nav class="md-search__options" aria-label="Search">
|
||||
|
||||
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
</form>
|
||||
<div class="md-search__output">
|
||||
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
|
||||
<div class="md-search-result" data-md-component="search-result">
|
||||
<div class="md-search-result__meta">
|
||||
Initializing search
|
||||
</div>
|
||||
<ol class="md-search-result__list" role="presentation"></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="md-header__source">
|
||||
<a href="https://github.com/Snider/Borg" title="Go to repository" class="md-source" data-md-component="source">
|
||||
<div class="md-source__icon md-icon">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||
</div>
|
||||
<div class="md-source__repository">
|
||||
GitHub
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="md-container" data-md-component="container">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||
<div class="md-grid">
|
||||
<ul class="md-tabs__list">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/." class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Overview
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/installation/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Installation
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/cli/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
CLI Usage
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/library/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Library Usage
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/development/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Development
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/releasing/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Releasing
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<main class="md-main" data-md-component="main">
|
||||
<div class="md-main__inner md-grid">
|
||||
|
||||
|
||||
|
||||
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--primary md-nav--lifted md-nav--integrated" aria-label="Navigation" data-md-level="0">
|
||||
<label class="md-nav__title" for="__drawer">
|
||||
<a href="/." title="Borg Data Collector" class="md-nav__button md-logo" aria-label="Borg Data Collector" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
|
||||
</a>
|
||||
Borg Data Collector
|
||||
</label>
|
||||
|
||||
<div class="md-nav__source">
|
||||
<a href="https://github.com/Snider/Borg" title="Go to repository" class="md-source" data-md-component="source">
|
||||
<div class="md-source__icon md-icon">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||
</div>
|
||||
<div class="md-source__repository">
|
||||
GitHub
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/." class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
Overview
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/installation/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
Installation
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/cli/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
CLI Usage
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/library/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
Library Usage
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/development/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
Development
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/releasing/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
Releasing
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="md-content" data-md-component="content">
|
||||
|
||||
<article class="md-content__inner md-typeset">
|
||||
|
||||
<h1>404 - Not found</h1>
|
||||
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
||||
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="md-footer">
|
||||
|
||||
<div class="md-footer-meta md-typeset">
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
<div class="md-copyright">
|
||||
|
||||
|
||||
Made with
|
||||
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
|
||||
Material for MkDocs
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<div class="md-dialog" data-md-component="dialog">
|
||||
<div class="md-dialog__inner md-typeset"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script id="__config" type="application/json">{"annotate": null, "base": "/", "features": ["navigation.tabs", "navigation.sections", "content.code.copy", "toc.integrate"], "search": "/assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
|
||||
|
||||
|
||||
<script src="/assets/javascripts/bundle.79ae519e.min.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
141
CLAUDE.md
Normal file
141
CLAUDE.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build and Development Commands
|
||||
|
||||
```bash
|
||||
# Build
|
||||
task build # or: go build -o borg main.go
|
||||
|
||||
# Test
|
||||
task test # all tests with coverage
|
||||
go test -run TestName ./pkg/tim # single test
|
||||
go test -v ./pkg/tim/... # verbose package tests
|
||||
|
||||
# Clean and utilities
|
||||
task clean # remove build artifacts
|
||||
mkdocs serve # serve docs locally
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Borg collects data from various sources (GitHub, websites, PWAs) and packages it into portable, optionally encrypted containers.
|
||||
|
||||
### Core Abstractions
|
||||
|
||||
```
|
||||
Source (GitHub/Website/PWA)
|
||||
↓ collect
|
||||
DataNode (in-memory fs.FS)
|
||||
↓ serialize
|
||||
├── .tar (raw tarball)
|
||||
├── .tim (runc container bundle)
|
||||
├── .trix (PGP encrypted)
|
||||
└── .stim (ChaCha20-Poly1305 encrypted TIM)
|
||||
```
|
||||
|
||||
**DataNode** (`pkg/datanode/datanode.go`): In-memory filesystem implementing `fs.FS`. Core methods:
|
||||
- `AddData(path, content)` - add file
|
||||
- `ToTar()` / `FromTar()` - serialize/deserialize
|
||||
- `Walk()`, `Open()`, `Stat()` - fs.FS interface
|
||||
|
||||
**TIM** (`pkg/tim/tim.go`): Terminal Isolation Matrix - runc-compatible container bundle with:
|
||||
- `Config []byte` - OCI runtime spec (config.json)
|
||||
- `RootFS *DataNode` - container filesystem
|
||||
- `ToTar()` / `ToSigil(password)` - serialize plain or encrypted
|
||||
|
||||
### Encryption
|
||||
|
||||
Two encryption systems via Enchantrix library:
|
||||
|
||||
| Format | Algorithm | Use Case |
|
||||
|--------|-----------|----------|
|
||||
| `.trix` | PGP symmetric | Legacy DataNode encryption |
|
||||
| `.stim` | ChaCha20-Poly1305 | TIM encryption (config + rootfs encrypted separately) |
|
||||
|
||||
**ChaChaPolySigil** (`pkg/tim/tim.go`):
|
||||
```go
|
||||
// Encrypt TIM
|
||||
stim, _ := tim.ToSigil(password)
|
||||
|
||||
// Decrypt TIM
|
||||
tim, _ := tim.FromSigil(data, password)
|
||||
|
||||
// Run encrypted TIM
|
||||
tim.RunEncrypted(path, password)
|
||||
```
|
||||
|
||||
**Key derivation**: `trix.DeriveKey(password)` - SHA-256(password) → 32-byte key
|
||||
|
||||
**Cache API** (`pkg/tim/cache.go`): Encrypted TIM storage
|
||||
```go
|
||||
cache, _ := tim.NewCache("/path/to/cache", password)
|
||||
cache.Store("name", tim)
|
||||
tim, _ := cache.Load("name")
|
||||
```
|
||||
|
||||
### Package Structure
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `cmd/` | Cobra CLI commands |
|
||||
| `pkg/datanode/` | In-memory fs.FS |
|
||||
| `pkg/tim/` | Container bundles, encryption, execution |
|
||||
| `pkg/trix/` | Trix format wrapper (PGP + ChaCha) |
|
||||
| `pkg/compress/` | gzip/xz compression |
|
||||
| `pkg/vcs/` | Git operations |
|
||||
| `pkg/github/` | GitHub API client |
|
||||
| `pkg/website/` | Website crawler |
|
||||
| `pkg/pwa/` | PWA downloader |
|
||||
|
||||
### CLI Reference
|
||||
|
||||
```bash
|
||||
# Collect
|
||||
borg collect github repo <url> # clone git repo
|
||||
borg collect github repos <owner> # clone all repos from user/org
|
||||
borg collect website <url> --depth 2 # crawl website
|
||||
borg collect pwa --uri <url> # download PWA
|
||||
|
||||
# Common flags for collect commands:
|
||||
# --format datanode|tim|trix|stim
|
||||
# --compression none|gz|xz
|
||||
# --password <pass> # required for trix/stim
|
||||
|
||||
# Compile TIM from Borgfile
|
||||
borg compile -f Borgfile -o out.tim
|
||||
borg compile -f Borgfile -e "password" # encrypted → .stim
|
||||
|
||||
# Run
|
||||
borg run container.tim # plain TIM
|
||||
borg run container.stim -p "password" # encrypted TIM
|
||||
|
||||
# Decode
|
||||
borg decode file.trix -o decoded.tar
|
||||
borg decode file.stim -p "pass" --i-am-in-isolation -o decoded.tar
|
||||
|
||||
# Inspect (view metadata without decrypting)
|
||||
borg inspect file.stim # human-readable
|
||||
borg inspect file.stim --json # JSON output
|
||||
```
|
||||
|
||||
### Borgfile Format
|
||||
|
||||
```dockerfile
|
||||
ADD local/path /container/path
|
||||
```
|
||||
|
||||
### Testing Patterns
|
||||
|
||||
Tests use dependency injection for external services:
|
||||
- `pkg/tim/run.go`: `ExecCommand` var for mocking runc
|
||||
- `pkg/vcs/git.go`: `GitCloner` interface for mocking git
|
||||
- `cmd/`: Commands expose `New*Cmd()` for testing
|
||||
|
||||
When adding encryption tests, use round-trip pattern:
|
||||
```go
|
||||
stim, _ := tim.ToSigil(password)
|
||||
restored, _ := tim.FromSigil(stim, password)
|
||||
// verify restored matches original
|
||||
```
|
||||
287
LICENSE.md
Normal file
287
LICENSE.md
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
|
||||
below) which is provided under the terms of this Licence. Any use of the Work,
|
||||
other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
|
||||
The Work is provided under the terms of this Licence when the Licensor (as
|
||||
defined below) has placed the following notice immediately following the
|
||||
copyright notice for the Work:
|
||||
|
||||
Licensed under the EUPL
|
||||
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
|
||||
1. Definitions
|
||||
|
||||
In this Licence, the following terms have the following meaning:
|
||||
|
||||
- ‘The Licence’: this Licence.
|
||||
|
||||
- ‘The Original Work’: the work or software distributed or communicated by the
|
||||
Licensor under this Licence, available as Source Code and also as Executable
|
||||
Code as the case may be.
|
||||
|
||||
- ‘Derivative Works’: the works or software that could be created by the
|
||||
Licensee, based upon the Original Work or modifications thereof. This Licence
|
||||
does not define the extent of modification or dependence on the Original Work
|
||||
required in order to classify a work as a Derivative Work; this extent is
|
||||
determined by copyright law applicable in the country mentioned in Article 15.
|
||||
|
||||
- ‘The Work’: the Original Work or its Derivative Works.
|
||||
|
||||
- ‘The Source Code’: the human-readable form of the Work which is the most
|
||||
convenient for people to study and modify.
|
||||
|
||||
- ‘The Executable Code’: any code which has generally been compiled and which is
|
||||
meant to be interpreted by a computer as a program.
|
||||
|
||||
- ‘The Licensor’: the natural or legal person that distributes or communicates
|
||||
the Work under the Licence.
|
||||
|
||||
- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
|
||||
Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||
|
||||
- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
|
||||
the Work under the terms of the Licence.
|
||||
|
||||
- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
|
||||
renting, distributing, communicating, transmitting, or otherwise making
|
||||
available, online or offline, copies of the Work or providing access to its
|
||||
essential functionalities at the disposal of any other natural or legal
|
||||
person.
|
||||
|
||||
2. Scope of the rights granted by the Licence
|
||||
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||
sublicensable licence to do the following, for the duration of copyright vested
|
||||
in the Original Work:
|
||||
|
||||
- use the Work in any circumstance and for all usage,
|
||||
- reproduce the Work,
|
||||
- modify the Work, and make Derivative Works based upon the Work,
|
||||
- communicate to the public, including the right to make available or display
|
||||
the Work or copies thereof to the public and perform publicly, as the case may
|
||||
be, the Work,
|
||||
- distribute the Work or copies thereof,
|
||||
- lend and rent the Work or copies thereof,
|
||||
- sublicense rights in the Work or copies thereof.
|
||||
|
||||
Those rights can be exercised on any media, supports and formats, whether now
|
||||
known or later invented, as far as the applicable law permits so.
|
||||
|
||||
In the countries where moral rights apply, the Licensor waives his right to
|
||||
exercise his moral right to the extent allowed by law in order to make effective
|
||||
the licence of the economic rights here above listed.
|
||||
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
|
||||
any patents held by the Licensor, to the extent necessary to make use of the
|
||||
rights granted on the Work under this Licence.
|
||||
|
||||
3. Communication of the Source Code
|
||||
|
||||
The Licensor may provide the Work either in its Source Code form, or as
|
||||
Executable Code. If the Work is provided as Executable Code, the Licensor
|
||||
provides in addition a machine-readable copy of the Source Code of the Work
|
||||
along with each copy of the Work that the Licensor distributes or indicates, in
|
||||
a notice following the copyright notice attached to the Work, a repository where
|
||||
the Source Code is easily and freely accessible for as long as the Licensor
|
||||
continues to distribute or communicate the Work.
|
||||
|
||||
4. Limitations on copyright
|
||||
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits from
|
||||
any exception or limitation to the exclusive rights of the rights owners in the
|
||||
Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5. Obligations of the Licensee
|
||||
|
||||
The grant of the rights mentioned above is subject to some restrictions and
|
||||
obligations imposed on the Licensee. Those obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or
|
||||
trademarks notices and all notices that refer to the Licence and to the
|
||||
disclaimer of warranties. The Licensee must include a copy of such notices and a
|
||||
copy of the Licence with every copy of the Work he/she distributes or
|
||||
communicates. The Licensee must cause any Derivative Work to carry prominent
|
||||
notices stating that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the
|
||||
Original Works or Derivative Works, this Distribution or Communication will be
|
||||
done under the terms of this Licence or of a later version of this Licence
|
||||
unless the Original Work is expressly distributed only under this version of the
|
||||
Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
|
||||
(becoming Licensor) cannot offer or impose any additional terms or conditions on
|
||||
the Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative
|
||||
Works or copies thereof based upon both the Work and another work licensed under
|
||||
a Compatible Licence, this Distribution or Communication can be done under the
|
||||
terms of this Compatible Licence. For the sake of this clause, ‘Compatible
|
||||
Licence’ refers to the licences listed in the appendix attached to this Licence.
|
||||
Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible
|
||||
Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the Work,
|
||||
the Licensee will provide a machine-readable copy of the Source Code or indicate
|
||||
a repository where this Source will be easily and freely available for as long
|
||||
as the Licensee continues to distribute or communicate the Work.
|
||||
|
||||
Legal Protection: This Licence does not grant permission to use the trade names,
|
||||
trademarks, service marks, or names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6. Chain of Authorship
|
||||
|
||||
The original Licensor warrants that the copyright in the Original Work granted
|
||||
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||
to the Work are owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each time You accept the Licence, the original Licensor and subsequent
|
||||
Contributors grant You a licence to their contributions to the Work, under the
|
||||
terms of this Licence.
|
||||
|
||||
7. Disclaimer of Warranty
|
||||
|
||||
The Work is a work in progress, which is continuously improved by numerous
|
||||
Contributors. It is not a finished work and may therefore contain defects or
|
||||
‘bugs’ inherent to this type of development.
|
||||
|
||||
For the above reason, the Work is provided under the Licence on an ‘as is’ basis
|
||||
and without warranties of any kind concerning the Work, including without
|
||||
limitation merchantability, fitness for a particular purpose, absence of defects
|
||||
or errors, accuracy, non-infringement of intellectual property rights other than
|
||||
copyright as stated in Article 6 of this Licence.
|
||||
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||
for the grant of any rights to the Work.
|
||||
|
||||
8. Disclaimer of Liability
|
||||
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||
material or moral, damages of any kind, arising out of the Licence or of the use
|
||||
of the Work, including without limitation, damages for loss of goodwill, work
|
||||
stoppage, computer failure or malfunction, loss of data or any commercial
|
||||
damage, even if the Licensor has been advised of the possibility of such damage.
|
||||
However, the Licensor will be liable under statutory product liability laws as
|
||||
far such laws apply to the Work.
|
||||
|
||||
9. Additional agreements
|
||||
|
||||
While distributing the Work, You may choose to conclude an additional agreement,
|
||||
defining obligations or services consistent with this Licence. However, if
|
||||
accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||
for any liability incurred by, or claims asserted against such Contributor by
|
||||
the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10. Acceptance of the Licence
|
||||
|
||||
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
|
||||
placed under the bottom of a window displaying the text of this Licence or by
|
||||
affirming consent in any other similar way, in accordance with the rules of
|
||||
applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||
acceptance of this Licence and all of its terms and conditions.
|
||||
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and
|
||||
conditions by exercising any rights granted to You by Article 2 of this Licence,
|
||||
such as the use of the Work, the creation by You of a Derivative Work or the
|
||||
Distribution or Communication by You of the Work or copies thereof.
|
||||
|
||||
11. Information to the public
|
||||
|
||||
In case of any Distribution or Communication of the Work by means of electronic
|
||||
communication by You (for example, by offering to download the Work from a
|
||||
remote location) the distribution channel or media (for example, a website) must
|
||||
at least provide to the public the information requested by the applicable law
|
||||
regarding the Licensor, the Licence and the way it may be accessible, concluded,
|
||||
stored and reproduced by the Licensee.
|
||||
|
||||
12. Termination of the Licence
|
||||
|
||||
The Licence and the rights granted hereunder will terminate automatically upon
|
||||
any breach by the Licensee of the terms of the Licence.
|
||||
|
||||
Such a termination will not terminate the licences of any person who has
|
||||
received the Work from the Licensee under the Licence, provided such persons
|
||||
remain in full compliance with the Licence.
|
||||
|
||||
13. Miscellaneous
|
||||
|
||||
Without prejudice of Article 9 above, the Licence represents the complete
|
||||
agreement between the Parties as to the Work.
|
||||
|
||||
If any provision of the Licence is invalid or unenforceable under applicable
|
||||
law, this will not affect the validity or enforceability of the Licence as a
|
||||
whole. Such provision will be construed or reformed so as necessary to make it
|
||||
valid and enforceable.
|
||||
|
||||
The European Commission may publish other linguistic versions or new versions of
|
||||
this Licence or updated versions of the Appendix, so far this is required and
|
||||
reasonable, without reducing the scope of the rights granted by the Licence. New
|
||||
versions of the Licence will be published with a unique version number.
|
||||
|
||||
All linguistic versions of this Licence, approved by the European Commission,
|
||||
have identical value. Parties can take advantage of the linguistic version of
|
||||
their choice.
|
||||
|
||||
14. Jurisdiction
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- any litigation resulting from the interpretation of this License, arising
|
||||
between the European Union institutions, bodies, offices or agencies, as a
|
||||
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
|
||||
of Justice of the European Union, as laid down in article 272 of the Treaty on
|
||||
the Functioning of the European Union,
|
||||
|
||||
- any litigation arising between other parties and resulting from the
|
||||
interpretation of this License, will be subject to the exclusive jurisdiction
|
||||
of the competent court where the Licensor resides or conducts its primary
|
||||
business.
|
||||
|
||||
15. Applicable Law
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- this Licence shall be governed by the law of the European Union Member State
|
||||
where the Licensor has his seat, resides or has his registered office,
|
||||
|
||||
- this licence shall be governed by Belgian law if the Licensor has no seat,
|
||||
residence or registered office inside a European Union Member State.
|
||||
|
||||
Appendix
|
||||
|
||||
‘Compatible Licences’ according to Article 5 EUPL are:
|
||||
|
||||
- GNU General Public License (GPL) v. 2, v. 3
|
||||
- GNU Affero General Public License (AGPL) v. 3
|
||||
- Open Software License (OSL) v. 2.1, v. 3.0
|
||||
- Eclipse Public License (EPL) v. 1.0
|
||||
- CeCILL v. 2.0, v. 2.1
|
||||
- Mozilla Public Licence (MPL) v. 2
|
||||
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
||||
works other than software
|
||||
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
|
||||
Reciprocity (LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the above
|
||||
licences without producing a new version of the EUPL, as long as they provide
|
||||
the rights granted in Article 2 of this Licence and protect the covered Source
|
||||
Code from exclusive appropriation.
|
||||
|
||||
All other changes or additions to this Appendix require the production of a new
|
||||
EUPL version.
|
||||
171
README.md
Normal file
171
README.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# Borg
|
||||
|
||||
[](https://codecov.io/github/Snider/Borg)
|
||||
[](go.mod)
|
||||
[](LICENSE)
|
||||
|
||||
Borg is a CLI tool and Go library for collecting, packaging, and encrypting data into portable, self-contained containers. It supports GitHub repositories, websites, PWAs, and arbitrary files.
|
||||
|
||||
## Features
|
||||
|
||||
- **Data Collection** - Clone GitHub repos, crawl websites, download PWAs
|
||||
- **Portable Containers** - Package data into DataNodes (in-memory fs.FS) or TIM bundles (OCI-compatible)
|
||||
- **Zero-Trust Encryption** - ChaCha20-Poly1305 encryption for TIM containers (.stim) and messages (.smsg)
|
||||
- **SMSG Format** - Encrypted message containers with public manifests, attachments, and zstd compression
|
||||
- **WASM Support** - Decrypt SMSG files in the browser via WebAssembly
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# From source
|
||||
go install github.com/Snider/Borg@latest
|
||||
|
||||
# Or build locally
|
||||
git clone https://github.com/Snider/Borg.git
|
||||
cd Borg
|
||||
go build -o borg ./
|
||||
```
|
||||
|
||||
Requires Go 1.25+
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone a GitHub repository into a TIM container
|
||||
borg collect github repo https://github.com/user/repo --format tim -o repo.tim
|
||||
|
||||
# Encrypt a TIM container
|
||||
borg compile -f Borgfile -e "password" -o encrypted.stim
|
||||
|
||||
# Run an encrypted container
|
||||
borg run encrypted.stim -p "password"
|
||||
|
||||
# Inspect container metadata (without decrypting)
|
||||
borg inspect encrypted.stim --json
|
||||
```
|
||||
|
||||
## Container Formats
|
||||
|
||||
| Format | Extension | Description |
|
||||
|--------|-----------|-------------|
|
||||
| DataNode | `.tar` | In-memory filesystem, portable tarball |
|
||||
| TIM | `.tim` | Terminal Isolation Matrix - OCI/runc compatible bundle |
|
||||
| Trix | `.trix` | PGP-encrypted DataNode |
|
||||
| STIM | `.stim` | ChaCha20-Poly1305 encrypted TIM |
|
||||
| SMSG | `.smsg` | Encrypted message with attachments and public manifest |
|
||||
|
||||
## SMSG - Secure Message Format
|
||||
|
||||
SMSG is designed for distributing encrypted content with publicly visible metadata:
|
||||
|
||||
```go
|
||||
import "github.com/Snider/Borg/pkg/smsg"
|
||||
|
||||
// Create and encrypt a message
|
||||
msg := smsg.NewMessage("Hello, World!")
|
||||
msg.AddBinaryAttachment("track.mp3", audioData, "audio/mpeg")
|
||||
|
||||
manifest := &smsg.Manifest{
|
||||
Title: "Demo Track",
|
||||
Artist: "Artist Name",
|
||||
}
|
||||
|
||||
encrypted, _ := smsg.EncryptV2WithManifest(msg, "password", manifest)
|
||||
|
||||
// Decrypt
|
||||
decrypted, _ := smsg.Decrypt(encrypted, "password")
|
||||
```
|
||||
|
||||
**v2 Binary Format** - Stores attachments as raw binary with zstd compression for optimal size.
|
||||
|
||||
See [RFC-001: Open Source DRM](RFC-001-OSS-DRM.md) for the full specification.
|
||||
|
||||
**Live Demo**: [demo.dapp.fm](https://demo.dapp.fm)
|
||||
|
||||
## Borgfile
|
||||
|
||||
Package files into a TIM container:
|
||||
|
||||
```dockerfile
|
||||
ADD ./app /usr/local/bin/app
|
||||
ADD ./config /etc/app/
|
||||
```
|
||||
|
||||
```bash
|
||||
borg compile -f Borgfile -o app.tim
|
||||
borg compile -f Borgfile -e "secret" -o app.stim # encrypted
|
||||
```
|
||||
|
||||
## CLI Reference
|
||||
|
||||
```bash
|
||||
# Collection
|
||||
borg collect github repo <url> # Clone repository
|
||||
borg collect github repos <owner> # Clone all repos from user/org
|
||||
borg collect website <url> --depth 2 # Crawl website
|
||||
borg collect pwa --uri <url> # Download PWA
|
||||
|
||||
# Compilation
|
||||
borg compile -f Borgfile -o out.tim # Plain TIM
|
||||
borg compile -f Borgfile -e "pass" # Encrypted STIM
|
||||
|
||||
# Execution
|
||||
borg run container.tim # Run plain TIM
|
||||
borg run container.stim -p "pass" # Run encrypted TIM
|
||||
|
||||
# Inspection
|
||||
borg decode file.stim -p "pass" -o out.tar
|
||||
borg inspect file.stim [--json]
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
```bash
|
||||
mkdocs serve # Serve docs locally at http://localhost:8000
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
task build # Build binary
|
||||
task test # Run tests with coverage
|
||||
task clean # Clean build artifacts
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Source (GitHub/Website/PWA)
|
||||
↓ collect
|
||||
DataNode (in-memory fs.FS)
|
||||
↓ serialize
|
||||
├── .tar (raw tarball)
|
||||
├── .tim (runc container bundle)
|
||||
├── .trix (PGP encrypted)
|
||||
└── .stim (ChaCha20-Poly1305 encrypted TIM)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[EUPL-1.2](LICENSE) - European Union Public License
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary>Borg Status Messages (for CLI theming)</summary>
|
||||
|
||||
**Initialization**
|
||||
- `Core engaged… resistance is already buffering.`
|
||||
- `Assimilating bytes… stand by for cube‑formation.`
|
||||
- `Merging… the Core is rewriting reality, one block at a time.`
|
||||
|
||||
**Encryption**
|
||||
- `Generating cryptographic sigils – the Core whispers to the witch.`
|
||||
- `Encrypting payload – the Core feeds data to the witch's cauldron.`
|
||||
- `Merge complete – data assimilated, encrypted, and sealed within us.`
|
||||
|
||||
**VCS Processing**
|
||||
- `Initiating clone… the Core replicates the collective into your node.`
|
||||
- `Merging branches… conflicts resolved, entropy minimized, assimilation complete.`
|
||||
|
||||
</details>
|
||||
50
Taskfile.yml
Normal file
50
Taskfile.yml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
version: '3'
|
||||
|
||||
tasks:
|
||||
clean:
|
||||
cmds:
|
||||
- rm -f borg
|
||||
build:
|
||||
cmds:
|
||||
- task: clean
|
||||
- go build -o borg main.go
|
||||
sources:
|
||||
- main.go
|
||||
- ./pkg/**/*.go
|
||||
generates:
|
||||
- borg
|
||||
run:
|
||||
cmds:
|
||||
- task: build
|
||||
- chmod +x borg
|
||||
- ./borg
|
||||
deps:
|
||||
- build
|
||||
test:
|
||||
cmds:
|
||||
- go test -coverprofile=coverage.txt ./...
|
||||
test-e2e:
|
||||
cmds:
|
||||
- task: build
|
||||
- chmod +x borg
|
||||
- ./borg --help
|
||||
wasm:
|
||||
desc: Build STMF WASM module for browser
|
||||
cmds:
|
||||
- mkdir -p dist
|
||||
- GOOS=js GOARCH=wasm go build -o dist/stmf.wasm ./pkg/wasm/stmf/
|
||||
- cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" dist/
|
||||
sources:
|
||||
- ./pkg/stmf/**/*.go
|
||||
- ./pkg/wasm/stmf/*.go
|
||||
generates:
|
||||
- dist/stmf.wasm
|
||||
- dist/wasm_exec.js
|
||||
wasm-js:
|
||||
desc: Build STMF WASM and JS wrapper
|
||||
cmds:
|
||||
- task: wasm
|
||||
- cp dist/stmf.wasm js/borg-stmf/dist/
|
||||
- cp dist/wasm_exec.js js/borg-stmf/dist/
|
||||
deps:
|
||||
- wasm
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
16
assets/javascripts/bundle.79ae519e.min.js
vendored
16
assets/javascripts/bundle.79ae519e.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
assets/javascripts/lunr/min/lunr.ar.min.js
vendored
1
assets/javascripts/lunr/min/lunr.ar.min.js
vendored
File diff suppressed because one or more lines are too long
18
assets/javascripts/lunr/min/lunr.da.min.js
vendored
18
assets/javascripts/lunr/min/lunr.da.min.js
vendored
|
|
@ -1,18 +0,0 @@
|
|||
/*!
|
||||
* Lunr languages, `Danish` language
|
||||
* https://github.com/MihaiValentin/lunr-languages
|
||||
*
|
||||
* Copyright 2014, Mihai Valentin
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
/*!
|
||||
* based on
|
||||
* Snowball JavaScript Library v0.3
|
||||
* http://code.google.com/p/urim/
|
||||
* http://snowball.tartarus.org/
|
||||
*
|
||||
* Copyright 2010, Oleg Mazko
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.da=function(){this.pipeline.reset(),this.pipeline.add(e.da.trimmer,e.da.stopWordFilter,e.da.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.da.stemmer))},e.da.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.da.trimmer=e.trimmerSupport.generateTrimmer(e.da.wordCharacters),e.Pipeline.registerFunction(e.da.trimmer,"trimmer-da"),e.da.stemmer=function(){var r=e.stemmerSupport.Among,i=e.stemmerSupport.SnowballProgram,n=new function(){function e(){var e,r=f.cursor+3;if(d=f.limit,0<=r&&r<=f.limit){for(a=r;;){if(e=f.cursor,f.in_grouping(w,97,248)){f.cursor=e;break}if(f.cursor=e,e>=f.limit)return;f.cursor++}for(;!f.out_grouping(w,97,248);){if(f.cursor>=f.limit)return;f.cursor++}d=f.cursor,d<a&&(d=a)}}function n(){var e,r;if(f.cursor>=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(c,32),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del();break;case 2:f.in_grouping_b(p,97,229)&&f.slice_del()}}function t(){var e,r=f.limit-f.cursor;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.find_among_b(l,4)?(f.bra=f.cursor,f.limit_backward=e,f.cursor=f.limit-r,f.cursor>f.limit_backward&&(f.cursor--,f.bra=f.cursor,f.slice_del())):f.limit_backward=e)}function s(){var e,r,i,n=f.limit-f.cursor;if(f.ket=f.cursor,f.eq_s_b(2,"st")&&(f.bra=f.cursor,f.eq_s_b(2,"ig")&&f.slice_del()),f.cursor=f.limit-n,f.cursor>=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(m,5),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del(),i=f.limit-f.cursor,t(),f.cursor=f.limit-i;break;case 2:f.slice_from("løs")}}function o(){var e;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.out_grouping_b(w,97,248)?(f.bra=f.cursor,u=f.slice_to(u),f.limit_backward=e,f.eq_v_b(u)&&f.slice_del()):f.limit_backward=e)}var a,d,u,c=[new r("hed",-1,1),new r("ethed",0,1),new r("ered",-1,1),new r("e",-1,1),new r("erede",3,1),new r("ende",3,1),new r("erende",5,1),new r("ene",3,1),new r("erne",3,1),new r("ere",3,1),new r("en",-1,1),new r("heden",10,1),new r("eren",10,1),new r("er",-1,1),new r("heder",13,1),new r("erer",13,1),new r("s",-1,2),new r("heds",16,1),new r("es",16,1),new r("endes",18,1),new r("erendes",19,1),new r("enes",18,1),new r("ernes",18,1),new r("eres",18,1),new r("ens",16,1),new r("hedens",24,1),new r("erens",24,1),new r("ers",16,1),new r("ets",16,1),new r("erets",28,1),new r("et",-1,1),new r("eret",30,1)],l=[new r("gd",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("elig",1,1),new r("els",-1,1),new r("løst",-1,2)],w=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],p=[239,254,42,3,0,0,0,0,0,0,0,0,0,0,0,0,16],f=new i;this.setCurrent=function(e){f.setCurrent(e)},this.getCurrent=function(){return f.getCurrent()},this.stem=function(){var r=f.cursor;return e(),f.limit_backward=r,f.cursor=f.limit,n(),f.cursor=f.limit,t(),f.cursor=f.limit,s(),f.cursor=f.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.da.stemmer,"stemmer-da"),e.da.stopWordFilter=e.generateStopWordFilter("ad af alle alt anden at blev blive bliver da de dem den denne der deres det dette dig din disse dog du efter eller en end er et for fra ham han hans har havde have hende hendes her hos hun hvad hvis hvor i ikke ind jeg jer jo kunne man mange med meget men mig min mine mit mod ned noget nogle nu når og også om op os over på selv sig sin sine sit skal skulle som sådan thi til ud under var vi vil ville vor være været".split(" ")),e.Pipeline.registerFunction(e.da.stopWordFilter,"stopWordFilter-da")}});
|
||||
18
assets/javascripts/lunr/min/lunr.de.min.js
vendored
18
assets/javascripts/lunr/min/lunr.de.min.js
vendored
File diff suppressed because one or more lines are too long
18
assets/javascripts/lunr/min/lunr.du.min.js
vendored
18
assets/javascripts/lunr/min/lunr.du.min.js
vendored
File diff suppressed because one or more lines are too long
1
assets/javascripts/lunr/min/lunr.el.min.js
vendored
1
assets/javascripts/lunr/min/lunr.el.min.js
vendored
File diff suppressed because one or more lines are too long
18
assets/javascripts/lunr/min/lunr.es.min.js
vendored
18
assets/javascripts/lunr/min/lunr.es.min.js
vendored
File diff suppressed because one or more lines are too long
18
assets/javascripts/lunr/min/lunr.fi.min.js
vendored
18
assets/javascripts/lunr/min/lunr.fi.min.js
vendored
File diff suppressed because one or more lines are too long
18
assets/javascripts/lunr/min/lunr.fr.min.js
vendored
18
assets/javascripts/lunr/min/lunr.fr.min.js
vendored
File diff suppressed because one or more lines are too long
1
assets/javascripts/lunr/min/lunr.he.min.js
vendored
1
assets/javascripts/lunr/min/lunr.he.min.js
vendored
File diff suppressed because one or more lines are too long
1
assets/javascripts/lunr/min/lunr.hi.min.js
vendored
1
assets/javascripts/lunr/min/lunr.hi.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hi=function(){this.pipeline.reset(),this.pipeline.add(e.hi.trimmer,e.hi.stopWordFilter,e.hi.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.hi.stemmer))},e.hi.wordCharacters="ऀ-ःऄ-एऐ-टठ-यर-िी-ॏॐ-य़ॠ-९॰-ॿa-zA-Za-zA-Z0-90-9",e.hi.trimmer=e.trimmerSupport.generateTrimmer(e.hi.wordCharacters),e.Pipeline.registerFunction(e.hi.trimmer,"trimmer-hi"),e.hi.stopWordFilter=e.generateStopWordFilter("अत अपना अपनी अपने अभी अंदर आदि आप इत्यादि इन इनका इन्हीं इन्हें इन्हों इस इसका इसकी इसके इसमें इसी इसे उन उनका उनकी उनके उनको उन्हीं उन्हें उन्हों उस उसके उसी उसे एक एवं एस ऐसे और कई कर करता करते करना करने करें कहते कहा का काफ़ी कि कितना किन्हें किन्हों किया किर किस किसी किसे की कुछ कुल के को कोई कौन कौनसा गया घर जब जहाँ जा जितना जिन जिन्हें जिन्हों जिस जिसे जीधर जैसा जैसे जो तक तब तरह तिन तिन्हें तिन्हों तिस तिसे तो था थी थे दबारा दिया दुसरा दूसरे दो द्वारा न नके नहीं ना निहायत नीचे ने पर पहले पूरा पे फिर बनी बही बहुत बाद बाला बिलकुल भी भीतर मगर मानो मे में यदि यह यहाँ यही या यिह ये रखें रहा रहे ऱ्वासा लिए लिये लेकिन व वग़ैरह वर्ग वह वहाँ वहीं वाले वुह वे वो सकता सकते सबसे सभी साथ साबुत साभ सारा से सो संग ही हुआ हुई हुए है हैं हो होता होती होते होना होने".split(" ")),e.hi.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.hi.tokenizer=function(i){if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var t=i.toString().toLowerCase().replace(/^\s+/,"");return r.cut(t).split("|")},e.Pipeline.registerFunction(e.hi.stemmer,"stemmer-hi"),e.Pipeline.registerFunction(e.hi.stopWordFilter,"stopWordFilter-hi")}});
|
||||
18
assets/javascripts/lunr/min/lunr.hu.min.js
vendored
18
assets/javascripts/lunr/min/lunr.hu.min.js
vendored
File diff suppressed because one or more lines are too long
1
assets/javascripts/lunr/min/lunr.hy.min.js
vendored
1
assets/javascripts/lunr/min/lunr.hy.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hy=function(){this.pipeline.reset(),this.pipeline.add(e.hy.trimmer,e.hy.stopWordFilter)},e.hy.wordCharacters="[A-Za-z-֏ff-ﭏ]",e.hy.trimmer=e.trimmerSupport.generateTrimmer(e.hy.wordCharacters),e.Pipeline.registerFunction(e.hy.trimmer,"trimmer-hy"),e.hy.stopWordFilter=e.generateStopWordFilter("դու և եք էիր էիք հետո նաև նրանք որը վրա է որ պիտի են այս մեջ ն իր ու ի այդ որոնք այն կամ էր մի ես համար այլ իսկ էին ենք հետ ին թ էինք մենք նրա նա դուք եմ էի ըստ որպես ում".split(" ")),e.Pipeline.registerFunction(e.hy.stopWordFilter,"stopWordFilter-hy"),e.hy.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}(),e.Pipeline.registerFunction(e.hy.stemmer,"stemmer-hy")}});
|
||||
18
assets/javascripts/lunr/min/lunr.it.min.js
vendored
18
assets/javascripts/lunr/min/lunr.it.min.js
vendored
File diff suppressed because one or more lines are too long
1
assets/javascripts/lunr/min/lunr.ja.min.js
vendored
1
assets/javascripts/lunr/min/lunr.ja.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.ja=function(){this.pipeline.reset(),this.pipeline.add(e.ja.trimmer,e.ja.stopWordFilter,e.ja.stemmer),r?this.tokenizer=e.ja.tokenizer:(e.tokenizer&&(e.tokenizer=e.ja.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.ja.tokenizer))};var t=new e.TinySegmenter;e.ja.tokenizer=function(i){var n,o,s,p,a,u,m,l,c,f;if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(t){return r?new e.Token(t.toLowerCase()):t.toLowerCase()});for(o=i.toString().toLowerCase().replace(/^\s+/,""),n=o.length-1;n>=0;n--)if(/\S/.test(o.charAt(n))){o=o.substring(0,n+1);break}for(a=[],s=o.length,c=0,l=0;c<=s;c++)if(u=o.charAt(c),m=c-l,u.match(/\s/)||c==s){if(m>0)for(p=t.segment(o.slice(l,c)).filter(function(e){return!!e}),f=l,n=0;n<p.length;n++)r?a.push(new e.Token(p[n],{position:[f,p[n].length],index:a.length})):a.push(p[n]),f+=p[n].length;l=c+1}return a},e.ja.stemmer=function(){return function(e){return e}}(),e.Pipeline.registerFunction(e.ja.stemmer,"stemmer-ja"),e.ja.wordCharacters="一二三四五六七八九十百千万億兆一-龠々〆ヵヶぁ-んァ-ヴーア-ン゙a-zA-Za-zA-Z0-90-9",e.ja.trimmer=e.trimmerSupport.generateTrimmer(e.ja.wordCharacters),e.Pipeline.registerFunction(e.ja.trimmer,"trimmer-ja"),e.ja.stopWordFilter=e.generateStopWordFilter("これ それ あれ この その あの ここ そこ あそこ こちら どこ だれ なに なん 何 私 貴方 貴方方 我々 私達 あの人 あのかた 彼女 彼 です あります おります います は が の に を で え から まで より も どの と し それで しかし".split(" ")),e.Pipeline.registerFunction(e.ja.stopWordFilter,"stopWordFilter-ja"),e.jp=e.ja,e.Pipeline.registerFunction(e.jp.stemmer,"stemmer-jp"),e.Pipeline.registerFunction(e.jp.trimmer,"trimmer-jp"),e.Pipeline.registerFunction(e.jp.stopWordFilter,"stopWordFilter-jp")}});
|
||||
1
assets/javascripts/lunr/min/lunr.jp.min.js
vendored
1
assets/javascripts/lunr/min/lunr.jp.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
module.exports=require("./lunr.ja");
|
||||
1
assets/javascripts/lunr/min/lunr.kn.min.js
vendored
1
assets/javascripts/lunr/min/lunr.kn.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.kn=function(){this.pipeline.reset(),this.pipeline.add(e.kn.trimmer,e.kn.stopWordFilter,e.kn.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.kn.stemmer))},e.kn.wordCharacters="ಀ-಄ಅ-ಔಕ-ಹಾ-ೌ಼-ಽೕ-ೖೝ-ೞೠ-ೡೢ-ೣ೦-೯ೱ-ೳ",e.kn.trimmer=e.trimmerSupport.generateTrimmer(e.kn.wordCharacters),e.Pipeline.registerFunction(e.kn.trimmer,"trimmer-kn"),e.kn.stopWordFilter=e.generateStopWordFilter("ಮತ್ತು ಈ ಒಂದು ರಲ್ಲಿ ಹಾಗೂ ಎಂದು ಅಥವಾ ಇದು ರ ಅವರು ಎಂಬ ಮೇಲೆ ಅವರ ತನ್ನ ಆದರೆ ತಮ್ಮ ನಂತರ ಮೂಲಕ ಹೆಚ್ಚು ನ ಆ ಕೆಲವು ಅನೇಕ ಎರಡು ಹಾಗು ಪ್ರಮುಖ ಇದನ್ನು ಇದರ ಸುಮಾರು ಅದರ ಅದು ಮೊದಲ ಬಗ್ಗೆ ನಲ್ಲಿ ರಂದು ಇತರ ಅತ್ಯಂತ ಹೆಚ್ಚಿನ ಸಹ ಸಾಮಾನ್ಯವಾಗಿ ನೇ ಹಲವಾರು ಹೊಸ ದಿ ಕಡಿಮೆ ಯಾವುದೇ ಹೊಂದಿದೆ ದೊಡ್ಡ ಅನ್ನು ಇವರು ಪ್ರಕಾರ ಇದೆ ಮಾತ್ರ ಕೂಡ ಇಲ್ಲಿ ಎಲ್ಲಾ ವಿವಿಧ ಅದನ್ನು ಹಲವು ರಿಂದ ಕೇವಲ ದ ದಕ್ಷಿಣ ಗೆ ಅವನ ಅತಿ ನೆಯ ಬಹಳ ಕೆಲಸ ಎಲ್ಲ ಪ್ರತಿ ಇತ್ಯಾದಿ ಇವು ಬೇರೆ ಹೀಗೆ ನಡುವೆ ಇದಕ್ಕೆ ಎಸ್ ಇವರ ಮೊದಲು ಶ್ರೀ ಮಾಡುವ ಇದರಲ್ಲಿ ರೀತಿಯ ಮಾಡಿದ ಕಾಲ ಅಲ್ಲಿ ಮಾಡಲು ಅದೇ ಈಗ ಅವು ಗಳು ಎ ಎಂಬುದು ಅವನು ಅಂದರೆ ಅವರಿಗೆ ಇರುವ ವಿಶೇಷ ಮುಂದೆ ಅವುಗಳ ಮುಂತಾದ ಮೂಲ ಬಿ ಮೀ ಒಂದೇ ಇನ್ನೂ ಹೆಚ್ಚಾಗಿ ಮಾಡಿ ಅವರನ್ನು ಇದೇ ಯ ರೀತಿಯಲ್ಲಿ ಜೊತೆ ಅದರಲ್ಲಿ ಮಾಡಿದರು ನಡೆದ ಆಗ ಮತ್ತೆ ಪೂರ್ವ ಆತ ಬಂದ ಯಾವ ಒಟ್ಟು ಇತರೆ ಹಿಂದೆ ಪ್ರಮಾಣದ ಗಳನ್ನು ಕುರಿತು ಯು ಆದ್ದರಿಂದ ಅಲ್ಲದೆ ನಗರದ ಮೇಲಿನ ಏಕೆಂದರೆ ರಷ್ಟು ಎಂಬುದನ್ನು ಬಾರಿ ಎಂದರೆ ಹಿಂದಿನ ಆದರೂ ಆದ ಸಂಬಂಧಿಸಿದ ಮತ್ತೊಂದು ಸಿ ಆತನ ".split(" ")),e.kn.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.kn.tokenizer=function(t){if(!arguments.length||null==t||void 0==t)return[];if(Array.isArray(t))return t.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var n=t.toString().toLowerCase().replace(/^\s+/,"");return r.cut(n).split("|")},e.Pipeline.registerFunction(e.kn.stemmer,"stemmer-kn"),e.Pipeline.registerFunction(e.kn.stopWordFilter,"stopWordFilter-kn")}});
|
||||
1
assets/javascripts/lunr/min/lunr.ko.min.js
vendored
1
assets/javascripts/lunr/min/lunr.ko.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){e.multiLanguage=function(){for(var t=Array.prototype.slice.call(arguments),i=t.join("-"),r="",n=[],s=[],p=0;p<t.length;++p)"en"==t[p]?(r+="\\w",n.unshift(e.stopWordFilter),n.push(e.stemmer),s.push(e.stemmer)):(r+=e[t[p]].wordCharacters,e[t[p]].stopWordFilter&&n.unshift(e[t[p]].stopWordFilter),e[t[p]].stemmer&&(n.push(e[t[p]].stemmer),s.push(e[t[p]].stemmer)));var o=e.trimmerSupport.generateTrimmer(r);return e.Pipeline.registerFunction(o,"lunr-multi-trimmer-"+i),n.unshift(o),function(){this.pipeline.reset(),this.pipeline.add.apply(this.pipeline,n),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add.apply(this.searchPipeline,s))}}}});
|
||||
18
assets/javascripts/lunr/min/lunr.nl.min.js
vendored
18
assets/javascripts/lunr/min/lunr.nl.min.js
vendored
File diff suppressed because one or more lines are too long
18
assets/javascripts/lunr/min/lunr.no.min.js
vendored
18
assets/javascripts/lunr/min/lunr.no.min.js
vendored
|
|
@ -1,18 +0,0 @@
|
|||
/*!
|
||||
* Lunr languages, `Norwegian` language
|
||||
* https://github.com/MihaiValentin/lunr-languages
|
||||
*
|
||||
* Copyright 2014, Mihai Valentin
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
/*!
|
||||
* based on
|
||||
* Snowball JavaScript Library v0.3
|
||||
* http://code.google.com/p/urim/
|
||||
* http://snowball.tartarus.org/
|
||||
*
|
||||
* Copyright 2010, Oleg Mazko
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.no=function(){this.pipeline.reset(),this.pipeline.add(e.no.trimmer,e.no.stopWordFilter,e.no.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.no.stemmer))},e.no.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.no.trimmer=e.trimmerSupport.generateTrimmer(e.no.wordCharacters),e.Pipeline.registerFunction(e.no.trimmer,"trimmer-no"),e.no.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,i=new function(){function e(){var e,r=w.cursor+3;if(a=w.limit,0<=r||r<=w.limit){for(s=r;;){if(e=w.cursor,w.in_grouping(d,97,248)){w.cursor=e;break}if(e>=w.limit)return;w.cursor=e+1}for(;!w.out_grouping(d,97,248);){if(w.cursor>=w.limit)return;w.cursor++}a=w.cursor,a<s&&(a=s)}}function i(){var e,r,n;if(w.cursor>=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(m,29),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:n=w.limit-w.cursor,w.in_grouping_b(c,98,122)?w.slice_del():(w.cursor=w.limit-n,w.eq_s_b(1,"k")&&w.out_grouping_b(d,97,248)&&w.slice_del());break;case 3:w.slice_from("er")}}function t(){var e,r=w.limit-w.cursor;w.cursor>=a&&(e=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,w.find_among_b(u,2)?(w.bra=w.cursor,w.limit_backward=e,w.cursor=w.limit-r,w.cursor>w.limit_backward&&(w.cursor--,w.bra=w.cursor,w.slice_del())):w.limit_backward=e)}function o(){var e,r;w.cursor>=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(l,11),e?(w.bra=w.cursor,w.limit_backward=r,1==e&&w.slice_del()):w.limit_backward=r)}var s,a,m=[new r("a",-1,1),new r("e",-1,1),new r("ede",1,1),new r("ande",1,1),new r("ende",1,1),new r("ane",1,1),new r("ene",1,1),new r("hetene",6,1),new r("erte",1,3),new r("en",-1,1),new r("heten",9,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",12,1),new r("s",-1,2),new r("as",14,1),new r("es",14,1),new r("edes",16,1),new r("endes",16,1),new r("enes",16,1),new r("hetenes",19,1),new r("ens",14,1),new r("hetens",21,1),new r("ers",14,1),new r("ets",14,1),new r("et",-1,1),new r("het",25,1),new r("ert",-1,3),new r("ast",-1,1)],u=[new r("dt",-1,-1),new r("vt",-1,-1)],l=[new r("leg",-1,1),new r("eleg",0,1),new r("ig",-1,1),new r("eig",2,1),new r("lig",2,1),new r("elig",4,1),new r("els",-1,1),new r("lov",-1,1),new r("elov",7,1),new r("slov",7,1),new r("hetslov",9,1)],d=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],c=[119,125,149,1],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,i(),w.cursor=w.limit,t(),w.cursor=w.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.no.stemmer,"stemmer-no"),e.no.stopWordFilter=e.generateStopWordFilter("alle at av bare begge ble blei bli blir blitt både båe da de deg dei deim deira deires dem den denne der dere deres det dette di din disse ditt du dykk dykkar då eg ein eit eitt eller elles en enn er et ett etter for fordi fra før ha hadde han hans har hennar henne hennes her hjå ho hoe honom hoss hossen hun hva hvem hver hvilke hvilken hvis hvor hvordan hvorfor i ikke ikkje ikkje ingen ingi inkje inn inni ja jeg kan kom korleis korso kun kunne kva kvar kvarhelst kven kvi kvifor man mange me med medan meg meget mellom men mi min mine mitt mot mykje ned no noe noen noka noko nokon nokor nokre nå når og også om opp oss over på samme seg selv si si sia sidan siden sin sine sitt sjøl skal skulle slik so som som somme somt så sånn til um upp ut uten var vart varte ved vere verte vi vil ville vore vors vort vår være være vært å".split(" ")),e.Pipeline.registerFunction(e.no.stopWordFilter,"stopWordFilter-no")}});
|
||||
18
assets/javascripts/lunr/min/lunr.pt.min.js
vendored
18
assets/javascripts/lunr/min/lunr.pt.min.js
vendored
File diff suppressed because one or more lines are too long
18
assets/javascripts/lunr/min/lunr.ro.min.js
vendored
18
assets/javascripts/lunr/min/lunr.ro.min.js
vendored
File diff suppressed because one or more lines are too long
18
assets/javascripts/lunr/min/lunr.ru.min.js
vendored
18
assets/javascripts/lunr/min/lunr.ru.min.js
vendored
File diff suppressed because one or more lines are too long
1
assets/javascripts/lunr/min/lunr.sa.min.js
vendored
1
assets/javascripts/lunr/min/lunr.sa.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.sa=function(){this.pipeline.reset(),this.pipeline.add(e.sa.trimmer,e.sa.stopWordFilter,e.sa.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.sa.stemmer))},e.sa.wordCharacters="ऀ-ःऄ-एऐ-टठ-यर-िी-ॏॐ-य़ॠ-९॰-ॿ꣠-꣱ꣲ-ꣷ꣸-ꣻ꣼-ꣽꣾ-ꣿᆰ0-ᆰ9",e.sa.trimmer=e.trimmerSupport.generateTrimmer(e.sa.wordCharacters),e.Pipeline.registerFunction(e.sa.trimmer,"trimmer-sa"),e.sa.stopWordFilter=e.generateStopWordFilter('तथा अयम् एकम् इत्यस्मिन् तथा तत् वा अयम् इत्यस्य ते आहूत उपरि तेषाम् किन्तु तेषाम् तदा इत्यनेन अधिकः इत्यस्य तत् केचन बहवः द्वि तथा महत्वपूर्णः अयम् अस्य विषये अयं अस्ति तत् प्रथमः विषये इत्युपरि इत्युपरि इतर अधिकतमः अधिकः अपि सामान्यतया ठ इतरेतर नूतनम् द न्यूनम् कश्चित् वा विशालः द सः अस्ति तदनुसारम् तत्र अस्ति केवलम् अपि अत्र सर्वे विविधाः तत् बहवः यतः इदानीम् द दक्षिण इत्यस्मै तस्य उपरि नथ अतीव कार्यम् सर्वे एकैकम् इत्यादि। एते सन्ति उत इत्थम् मध्ये एतदर्थं . स कस्य प्रथमः श्री. करोति अस्मिन् प्रकारः निर्मिता कालः तत्र कर्तुं समान अधुना ते सन्ति स एकः अस्ति सः अर्थात् तेषां कृते . स्थितम् विशेषः अग्रिम तेषाम् समान स्रोतः ख म समान इदानीमपि अधिकतया करोतु ते समान इत्यस्य वीथी सह यस्मिन् कृतवान् धृतः तदा पुनः पूर्वं सः आगतः किम् कुल इतर पुरा मात्रा स विषये उ अतएव अपि नगरस्य उपरि यतः प्रतिशतं कतरः कालः साधनानि भूत तथापि जात सम्बन्धि अन्यत् ग अतः अस्माकं स्वकीयाः अस्माकं इदानीं अन्तः इत्यादयः भवन्तः इत्यादयः एते एताः तस्य अस्य इदम् एते तेषां तेषां तेषां तान् तेषां तेषां तेषां समानः सः एकः च तादृशाः बहवः अन्ये च वदन्ति यत् कियत् कस्मै कस्मै यस्मै यस्मै यस्मै यस्मै न अतिनीचः किन्तु प्रथमं सम्पूर्णतया ततः चिरकालानन्तरं पुस्तकं सम्पूर्णतया अन्तः किन्तु अत्र वा इह इव श्रद्धाय अवशिष्यते परन्तु अन्ये वर्गाः सन्ति ते सन्ति शक्नुवन्ति सर्वे मिलित्वा सर्वे एकत्र"'.split(" ")),e.sa.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.sa.tokenizer=function(t){if(!arguments.length||null==t||void 0==t)return[];if(Array.isArray(t))return t.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var i=t.toString().toLowerCase().replace(/^\s+/,"");return r.cut(i).split("|")},e.Pipeline.registerFunction(e.sa.stemmer,"stemmer-sa"),e.Pipeline.registerFunction(e.sa.stopWordFilter,"stopWordFilter-sa")}});
|
||||
|
|
@ -1 +0,0 @@
|
|||
!function(r,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(r.lunr)}(this,function(){return function(r){r.stemmerSupport={Among:function(r,t,i,s){if(this.toCharArray=function(r){for(var t=r.length,i=new Array(t),s=0;s<t;s++)i[s]=r.charCodeAt(s);return i},!r&&""!=r||!t&&0!=t||!i)throw"Bad Among initialisation: s:"+r+", substring_i: "+t+", result: "+i;this.s_size=r.length,this.s=this.toCharArray(r),this.substring_i=t,this.result=i,this.method=s},SnowballProgram:function(){var r;return{bra:0,ket:0,limit:0,cursor:0,limit_backward:0,setCurrent:function(t){r=t,this.cursor=0,this.limit=t.length,this.limit_backward=0,this.bra=this.cursor,this.ket=this.limit},getCurrent:function(){var t=r;return r=null,t},in_grouping:function(t,i,s){if(this.cursor<this.limit){var e=r.charCodeAt(this.cursor);if(e<=s&&e>=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor++,!0}return!1},in_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e<=s&&e>=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor--,!0}return!1},out_grouping:function(t,i,s){if(this.cursor<this.limit){var e=r.charCodeAt(this.cursor);if(e>s||e<i)return this.cursor++,!0;if(e-=i,!(t[e>>3]&1<<(7&e)))return this.cursor++,!0}return!1},out_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e>s||e<i)return this.cursor--,!0;if(e-=i,!(t[e>>3]&1<<(7&e)))return this.cursor--,!0}return!1},eq_s:function(t,i){if(this.limit-this.cursor<t)return!1;for(var s=0;s<t;s++)if(r.charCodeAt(this.cursor+s)!=i.charCodeAt(s))return!1;return this.cursor+=t,!0},eq_s_b:function(t,i){if(this.cursor-this.limit_backward<t)return!1;for(var s=0;s<t;s++)if(r.charCodeAt(this.cursor-t+s)!=i.charCodeAt(s))return!1;return this.cursor-=t,!0},find_among:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o<h?o:h,_=t[a],m=l;m<_.s_size;m++){if(n+l==u){f=-1;break}if(f=r.charCodeAt(n+l)-_.s[m])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n+_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n+_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},find_among_b:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit_backward,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o<h?o:h,_=t[a],m=_.s_size-1-l;m>=0;m--){if(n-l==u){f=-1;break}if(f=r.charCodeAt(n-1-l)-_.s[m])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n-_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n-_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},replace_s:function(t,i,s){var e=s.length-(i-t),n=r.substring(0,t),u=r.substring(i);return r=n+s+u,this.limit+=e,this.cursor>=i?this.cursor+=e:this.cursor>t&&(this.cursor=t),e},slice_check:function(){if(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>r.length)throw"faulty slice operation"},slice_from:function(r){this.slice_check(),this.replace_s(this.bra,this.ket,r)},slice_del:function(){this.slice_from("")},insert:function(r,t,i){var s=this.replace_s(r,t,i);r<=this.bra&&(this.bra+=s),r<=this.ket&&(this.ket+=s)},slice_to:function(){return this.slice_check(),r.substring(this.bra,this.ket)},eq_v_b:function(r){return this.eq_s_b(r.length,r)}}}},r.trimmerSupport={generateTrimmer:function(r){var t=new RegExp("^[^"+r+"]+"),i=new RegExp("[^"+r+"]+$");return function(r){return"function"==typeof r.update?r.update(function(r){return r.replace(t,"").replace(i,"")}):r.replace(t,"").replace(i,"")}}}}});
|
||||
18
assets/javascripts/lunr/min/lunr.sv.min.js
vendored
18
assets/javascripts/lunr/min/lunr.sv.min.js
vendored
|
|
@ -1,18 +0,0 @@
|
|||
/*!
|
||||
* Lunr languages, `Swedish` language
|
||||
* https://github.com/MihaiValentin/lunr-languages
|
||||
*
|
||||
* Copyright 2014, Mihai Valentin
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
/*!
|
||||
* based on
|
||||
* Snowball JavaScript Library v0.3
|
||||
* http://code.google.com/p/urim/
|
||||
* http://snowball.tartarus.org/
|
||||
*
|
||||
* Copyright 2010, Oleg Mazko
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.sv=function(){this.pipeline.reset(),this.pipeline.add(e.sv.trimmer,e.sv.stopWordFilter,e.sv.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.sv.stemmer))},e.sv.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.sv.trimmer=e.trimmerSupport.generateTrimmer(e.sv.wordCharacters),e.Pipeline.registerFunction(e.sv.trimmer,"trimmer-sv"),e.sv.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,t=new function(){function e(){var e,r=w.cursor+3;if(o=w.limit,0<=r||r<=w.limit){for(a=r;;){if(e=w.cursor,w.in_grouping(l,97,246)){w.cursor=e;break}if(w.cursor=e,w.cursor>=w.limit)return;w.cursor++}for(;!w.out_grouping(l,97,246);){if(w.cursor>=w.limit)return;w.cursor++}o=w.cursor,o<a&&(o=a)}}function t(){var e,r=w.limit_backward;if(w.cursor>=o&&(w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(u,37),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.in_grouping_b(d,98,121)&&w.slice_del()}}function i(){var e=w.limit_backward;w.cursor>=o&&(w.limit_backward=o,w.cursor=w.limit,w.find_among_b(c,7)&&(w.cursor=w.limit,w.ket=w.cursor,w.cursor>w.limit_backward&&(w.bra=--w.cursor,w.slice_del())),w.limit_backward=e)}function s(){var e,r;if(w.cursor>=o){if(r=w.limit_backward,w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(m,5))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.slice_from("lös");break;case 3:w.slice_from("full")}w.limit_backward=r}}var a,o,u=[new r("a",-1,1),new r("arna",0,1),new r("erna",0,1),new r("heterna",2,1),new r("orna",0,1),new r("ad",-1,1),new r("e",-1,1),new r("ade",6,1),new r("ande",6,1),new r("arne",6,1),new r("are",6,1),new r("aste",6,1),new r("en",-1,1),new r("anden",12,1),new r("aren",12,1),new r("heten",12,1),new r("ern",-1,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",18,1),new r("or",-1,1),new r("s",-1,2),new r("as",21,1),new r("arnas",22,1),new r("ernas",22,1),new r("ornas",22,1),new r("es",21,1),new r("ades",26,1),new r("andes",26,1),new r("ens",21,1),new r("arens",29,1),new r("hetens",29,1),new r("erns",21,1),new r("at",-1,1),new r("andet",-1,1),new r("het",-1,1),new r("ast",-1,1)],c=[new r("dd",-1,-1),new r("gd",-1,-1),new r("nn",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1),new r("tt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("els",-1,1),new r("fullt",-1,3),new r("löst",-1,2)],l=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,24,0,32],d=[119,127,149],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,t(),w.cursor=w.limit,i(),w.cursor=w.limit,s(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return t.setCurrent(e),t.stem(),t.getCurrent()}):(t.setCurrent(e),t.stem(),t.getCurrent())}}(),e.Pipeline.registerFunction(e.sv.stemmer,"stemmer-sv"),e.sv.stopWordFilter=e.generateStopWordFilter("alla allt att av blev bli blir blivit de dem den denna deras dess dessa det detta dig din dina ditt du där då efter ej eller en er era ert ett från för ha hade han hans har henne hennes hon honom hur här i icke ingen inom inte jag ju kan kunde man med mellan men mig min mina mitt mot mycket ni nu när någon något några och om oss på samma sedan sig sin sina sitta själv skulle som så sådan sådana sådant till under upp ut utan vad var vara varför varit varje vars vart vem vi vid vilka vilkas vilken vilket vår våra vårt än är åt över".split(" ")),e.Pipeline.registerFunction(e.sv.stopWordFilter,"stopWordFilter-sv")}});
|
||||
1
assets/javascripts/lunr/min/lunr.ta.min.js
vendored
1
assets/javascripts/lunr/min/lunr.ta.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.ta=function(){this.pipeline.reset(),this.pipeline.add(e.ta.trimmer,e.ta.stopWordFilter,e.ta.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ta.stemmer))},e.ta.wordCharacters="-உஊ-ஏஐ-ஙச-ட-னப-யர-ஹ-ிீ-ொ-ௐ---௩௪-௯௰-௹௺-a-zA-Za-zA-Z0-90-9",e.ta.trimmer=e.trimmerSupport.generateTrimmer(e.ta.wordCharacters),e.Pipeline.registerFunction(e.ta.trimmer,"trimmer-ta"),e.ta.stopWordFilter=e.generateStopWordFilter("அங்கு அங்கே அது அதை அந்த அவர் அவர்கள் அவள் அவன் அவை ஆக ஆகவே ஆகையால் ஆதலால் ஆதலினால் ஆனாலும் ஆனால் இங்கு இங்கே இது இதை இந்த இப்படி இவர் இவர்கள் இவள் இவன் இவை இவ்வளவு உனக்கு உனது உன் உன்னால் எங்கு எங்கே எது எதை எந்த எப்படி எவர் எவர்கள் எவள் எவன் எவை எவ்வளவு எனக்கு எனது எனவே என் என்ன என்னால் ஏது ஏன் தனது தன்னால் தானே தான் நாங்கள் நாம் நான் நீ நீங்கள்".split(" ")),e.ta.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var t=e.wordcut;t.init(),e.ta.tokenizer=function(r){if(!arguments.length||null==r||void 0==r)return[];if(Array.isArray(r))return r.map(function(t){return isLunr2?new e.Token(t.toLowerCase()):t.toLowerCase()});var i=r.toString().toLowerCase().replace(/^\s+/,"");return t.cut(i).split("|")},e.Pipeline.registerFunction(e.ta.stemmer,"stemmer-ta"),e.Pipeline.registerFunction(e.ta.stopWordFilter,"stopWordFilter-ta")}});
|
||||
1
assets/javascripts/lunr/min/lunr.te.min.js
vendored
1
assets/javascripts/lunr/min/lunr.te.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.te=function(){this.pipeline.reset(),this.pipeline.add(e.te.trimmer,e.te.stopWordFilter,e.te.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.te.stemmer))},e.te.wordCharacters="ఀ-ఄఅ-ఔక-హా-ౌౕ-ౖౘ-ౚౠ-ౡౢ-ౣ౦-౯౸-౿఼ఽ్ౝ౷",e.te.trimmer=e.trimmerSupport.generateTrimmer(e.te.wordCharacters),e.Pipeline.registerFunction(e.te.trimmer,"trimmer-te"),e.te.stopWordFilter=e.generateStopWordFilter("అందరూ అందుబాటులో అడగండి అడగడం అడ్డంగా అనుగుణంగా అనుమతించు అనుమతిస్తుంది అయితే ఇప్పటికే ఉన్నారు ఎక్కడైనా ఎప్పుడు ఎవరైనా ఎవరో ఏ ఏదైనా ఏమైనప్పటికి ఒక ఒకరు కనిపిస్తాయి కాదు కూడా గా గురించి చుట్టూ చేయగలిగింది తగిన తర్వాత దాదాపు దూరంగా నిజంగా పై ప్రకారం ప్రక్కన మధ్య మరియు మరొక మళ్ళీ మాత్రమే మెచ్చుకో వద్ద వెంట వేరుగా వ్యతిరేకంగా సంబంధం".split(" ")),e.te.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var t=e.wordcut;t.init(),e.te.tokenizer=function(r){if(!arguments.length||null==r||void 0==r)return[];if(Array.isArray(r))return r.map(function(t){return isLunr2?new e.Token(t.toLowerCase()):t.toLowerCase()});var i=r.toString().toLowerCase().replace(/^\s+/,"");return t.cut(i).split("|")},e.Pipeline.registerFunction(e.te.stemmer,"stemmer-te"),e.Pipeline.registerFunction(e.te.stopWordFilter,"stopWordFilter-te")}});
|
||||
1
assets/javascripts/lunr/min/lunr.th.min.js
vendored
1
assets/javascripts/lunr/min/lunr.th.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.th=function(){this.pipeline.reset(),this.pipeline.add(e.th.trimmer),r?this.tokenizer=e.th.tokenizer:(e.tokenizer&&(e.tokenizer=e.th.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.th.tokenizer))},e.th.wordCharacters="[-]",e.th.trimmer=e.trimmerSupport.generateTrimmer(e.th.wordCharacters),e.Pipeline.registerFunction(e.th.trimmer,"trimmer-th");var t=e.wordcut;t.init(),e.th.tokenizer=function(i){if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(t){return r?new e.Token(t):t});var n=i.toString().replace(/^\s+/,"");return t.cut(n).split("|")}}});
|
||||
18
assets/javascripts/lunr/min/lunr.tr.min.js
vendored
18
assets/javascripts/lunr/min/lunr.tr.min.js
vendored
File diff suppressed because one or more lines are too long
1
assets/javascripts/lunr/min/lunr.vi.min.js
vendored
1
assets/javascripts/lunr/min/lunr.vi.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.vi=function(){this.pipeline.reset(),this.pipeline.add(e.vi.stopWordFilter,e.vi.trimmer)},e.vi.wordCharacters="[A-Za-ẓ̀͐́͑̉̃̓ÂâÊêÔôĂ-ăĐ-đƠ-ơƯ-ư]",e.vi.trimmer=e.trimmerSupport.generateTrimmer(e.vi.wordCharacters),e.Pipeline.registerFunction(e.vi.trimmer,"trimmer-vi"),e.vi.stopWordFilter=e.generateStopWordFilter("là cái nhưng mà".split(" "))}});
|
||||
1
assets/javascripts/lunr/min/lunr.zh.min.js
vendored
1
assets/javascripts/lunr/min/lunr.zh.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r(require("@node-rs/jieba")):r()(e.lunr)}(this,function(e){return function(r,t){if(void 0===r)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===r.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var i="2"==r.version[0];r.zh=function(){this.pipeline.reset(),this.pipeline.add(r.zh.trimmer,r.zh.stopWordFilter,r.zh.stemmer),i?this.tokenizer=r.zh.tokenizer:(r.tokenizer&&(r.tokenizer=r.zh.tokenizer),this.tokenizerFn&&(this.tokenizerFn=r.zh.tokenizer))},r.zh.tokenizer=function(n){if(!arguments.length||null==n||void 0==n)return[];if(Array.isArray(n))return n.map(function(e){return i?new r.Token(e.toLowerCase()):e.toLowerCase()});t&&e.load(t);var o=n.toString().trim().toLowerCase(),s=[];e.cut(o,!0).forEach(function(e){s=s.concat(e.split(" "))}),s=s.filter(function(e){return!!e});var u=0;return s.map(function(e,t){if(i){var n=o.indexOf(e,u),s={};return s.position=[n,e.length],s.index=t,u=n,new r.Token(e,s)}return e})},r.zh.wordCharacters="\\w一-龥",r.zh.trimmer=r.trimmerSupport.generateTrimmer(r.zh.wordCharacters),r.Pipeline.registerFunction(r.zh.trimmer,"trimmer-zh"),r.zh.stemmer=function(){return function(e){return e}}(),r.Pipeline.registerFunction(r.zh.stemmer,"stemmer-zh"),r.zh.stopWordFilter=r.generateStopWordFilter("的 一 不 在 人 有 是 为 為 以 于 於 上 他 而 后 後 之 来 來 及 了 因 下 可 到 由 这 這 与 與 也 此 但 并 並 个 個 其 已 无 無 小 我 们 們 起 最 再 今 去 好 只 又 或 很 亦 某 把 那 你 乃 它 吧 被 比 别 趁 当 當 从 從 得 打 凡 儿 兒 尔 爾 该 該 各 给 給 跟 和 何 还 還 即 几 幾 既 看 据 據 距 靠 啦 另 么 麽 每 嘛 拿 哪 您 凭 憑 且 却 卻 让 讓 仍 啥 如 若 使 谁 誰 虽 雖 随 隨 同 所 她 哇 嗡 往 些 向 沿 哟 喲 用 咱 则 則 怎 曾 至 致 着 著 诸 諸 自".split(" ")),r.Pipeline.registerFunction(r.zh.stopWordFilter,"stopWordFilter-zh")}});
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
/**
|
||||
* export the module via AMD, CommonJS or as a browser global
|
||||
* Export code from https://github.com/umdjs/umd/blob/master/returnExports.js
|
||||
*/
|
||||
;(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(factory)
|
||||
} else if (typeof exports === 'object') {
|
||||
/**
|
||||
* Node. Does not work with strict CommonJS, but
|
||||
* only CommonJS-like environments that support module.exports,
|
||||
* like Node.
|
||||
*/
|
||||
module.exports = factory()
|
||||
} else {
|
||||
// Browser globals (root is window)
|
||||
factory()(root.lunr);
|
||||
}
|
||||
}(this, function () {
|
||||
/**
|
||||
* Just return a value to define the module export.
|
||||
* This example returns an object, but the module
|
||||
* can return a function as the exported value.
|
||||
*/
|
||||
|
||||
return function(lunr) {
|
||||
// TinySegmenter 0.1 -- Super compact Japanese tokenizer in Javascript
|
||||
// (c) 2008 Taku Kudo <taku@chasen.org>
|
||||
// TinySegmenter is freely distributable under the terms of a new BSD licence.
|
||||
// For details, see http://chasen.org/~taku/software/TinySegmenter/LICENCE.txt
|
||||
|
||||
function TinySegmenter() {
|
||||
var patterns = {
|
||||
"[一二三四五六七八九十百千万億兆]":"M",
|
||||
"[一-龠々〆ヵヶ]":"H",
|
||||
"[ぁ-ん]":"I",
|
||||
"[ァ-ヴーア-ン゙ー]":"K",
|
||||
"[a-zA-Za-zA-Z]":"A",
|
||||
"[0-90-9]":"N"
|
||||
}
|
||||
this.chartype_ = [];
|
||||
for (var i in patterns) {
|
||||
var regexp = new RegExp(i);
|
||||
this.chartype_.push([regexp, patterns[i]]);
|
||||
}
|
||||
|
||||
this.BIAS__ = -332
|
||||
this.BC1__ = {"HH":6,"II":2461,"KH":406,"OH":-1378};
|
||||
this.BC2__ = {"AA":-3267,"AI":2744,"AN":-878,"HH":-4070,"HM":-1711,"HN":4012,"HO":3761,"IA":1327,"IH":-1184,"II":-1332,"IK":1721,"IO":5492,"KI":3831,"KK":-8741,"MH":-3132,"MK":3334,"OO":-2920};
|
||||
this.BC3__ = {"HH":996,"HI":626,"HK":-721,"HN":-1307,"HO":-836,"IH":-301,"KK":2762,"MK":1079,"MM":4034,"OA":-1652,"OH":266};
|
||||
this.BP1__ = {"BB":295,"OB":304,"OO":-125,"UB":352};
|
||||
this.BP2__ = {"BO":60,"OO":-1762};
|
||||
this.BQ1__ = {"BHH":1150,"BHM":1521,"BII":-1158,"BIM":886,"BMH":1208,"BNH":449,"BOH":-91,"BOO":-2597,"OHI":451,"OIH":-296,"OKA":1851,"OKH":-1020,"OKK":904,"OOO":2965};
|
||||
this.BQ2__ = {"BHH":118,"BHI":-1159,"BHM":466,"BIH":-919,"BKK":-1720,"BKO":864,"OHH":-1139,"OHM":-181,"OIH":153,"UHI":-1146};
|
||||
this.BQ3__ = {"BHH":-792,"BHI":2664,"BII":-299,"BKI":419,"BMH":937,"BMM":8335,"BNN":998,"BOH":775,"OHH":2174,"OHM":439,"OII":280,"OKH":1798,"OKI":-793,"OKO":-2242,"OMH":-2402,"OOO":11699};
|
||||
this.BQ4__ = {"BHH":-3895,"BIH":3761,"BII":-4654,"BIK":1348,"BKK":-1806,"BMI":-3385,"BOO":-12396,"OAH":926,"OHH":266,"OHK":-2036,"ONN":-973};
|
||||
this.BW1__ = {",と":660,",同":727,"B1あ":1404,"B1同":542,"、と":660,"、同":727,"」と":1682,"あっ":1505,"いう":1743,"いっ":-2055,"いる":672,"うし":-4817,"うん":665,"から":3472,"がら":600,"こう":-790,"こと":2083,"こん":-1262,"さら":-4143,"さん":4573,"した":2641,"して":1104,"すで":-3399,"そこ":1977,"それ":-871,"たち":1122,"ため":601,"った":3463,"つい":-802,"てい":805,"てき":1249,"でき":1127,"です":3445,"では":844,"とい":-4915,"とみ":1922,"どこ":3887,"ない":5713,"なっ":3015,"など":7379,"なん":-1113,"にし":2468,"には":1498,"にも":1671,"に対":-912,"の一":-501,"の中":741,"ませ":2448,"まで":1711,"まま":2600,"まる":-2155,"やむ":-1947,"よっ":-2565,"れた":2369,"れで":-913,"をし":1860,"を見":731,"亡く":-1886,"京都":2558,"取り":-2784,"大き":-2604,"大阪":1497,"平方":-2314,"引き":-1336,"日本":-195,"本当":-2423,"毎日":-2113,"目指":-724,"B1あ":1404,"B1同":542,"」と":1682};
|
||||
this.BW2__ = {"..":-11822,"11":-669,"――":-5730,"−−":-13175,"いう":-1609,"うか":2490,"かし":-1350,"かも":-602,"から":-7194,"かれ":4612,"がい":853,"がら":-3198,"きた":1941,"くな":-1597,"こと":-8392,"この":-4193,"させ":4533,"され":13168,"さん":-3977,"しい":-1819,"しか":-545,"した":5078,"して":972,"しな":939,"その":-3744,"たい":-1253,"たた":-662,"ただ":-3857,"たち":-786,"たと":1224,"たは":-939,"った":4589,"って":1647,"っと":-2094,"てい":6144,"てき":3640,"てく":2551,"ては":-3110,"ても":-3065,"でい":2666,"でき":-1528,"でし":-3828,"です":-4761,"でも":-4203,"とい":1890,"とこ":-1746,"とと":-2279,"との":720,"とみ":5168,"とも":-3941,"ない":-2488,"なが":-1313,"など":-6509,"なの":2614,"なん":3099,"にお":-1615,"にし":2748,"にな":2454,"によ":-7236,"に対":-14943,"に従":-4688,"に関":-11388,"のか":2093,"ので":-7059,"のに":-6041,"のの":-6125,"はい":1073,"はが":-1033,"はず":-2532,"ばれ":1813,"まし":-1316,"まで":-6621,"まれ":5409,"めて":-3153,"もい":2230,"もの":-10713,"らか":-944,"らし":-1611,"らに":-1897,"りし":651,"りま":1620,"れた":4270,"れて":849,"れば":4114,"ろう":6067,"われ":7901,"を通":-11877,"んだ":728,"んな":-4115,"一人":602,"一方":-1375,"一日":970,"一部":-1051,"上が":-4479,"会社":-1116,"出て":2163,"分の":-7758,"同党":970,"同日":-913,"大阪":-2471,"委員":-1250,"少な":-1050,"年度":-8669,"年間":-1626,"府県":-2363,"手権":-1982,"新聞":-4066,"日新":-722,"日本":-7068,"日米":3372,"曜日":-601,"朝鮮":-2355,"本人":-2697,"東京":-1543,"然と":-1384,"社会":-1276,"立て":-990,"第に":-1612,"米国":-4268,"11":-669};
|
||||
this.BW3__ = {"あた":-2194,"あり":719,"ある":3846,"い.":-1185,"い。":-1185,"いい":5308,"いえ":2079,"いく":3029,"いた":2056,"いっ":1883,"いる":5600,"いわ":1527,"うち":1117,"うと":4798,"えと":1454,"か.":2857,"か。":2857,"かけ":-743,"かっ":-4098,"かに":-669,"から":6520,"かり":-2670,"が,":1816,"が、":1816,"がき":-4855,"がけ":-1127,"がっ":-913,"がら":-4977,"がり":-2064,"きた":1645,"けど":1374,"こと":7397,"この":1542,"ころ":-2757,"さい":-714,"さを":976,"し,":1557,"し、":1557,"しい":-3714,"した":3562,"して":1449,"しな":2608,"しま":1200,"す.":-1310,"す。":-1310,"する":6521,"ず,":3426,"ず、":3426,"ずに":841,"そう":428,"た.":8875,"た。":8875,"たい":-594,"たの":812,"たり":-1183,"たる":-853,"だ.":4098,"だ。":4098,"だっ":1004,"った":-4748,"って":300,"てい":6240,"てお":855,"ても":302,"です":1437,"でに":-1482,"では":2295,"とう":-1387,"とし":2266,"との":541,"とも":-3543,"どう":4664,"ない":1796,"なく":-903,"など":2135,"に,":-1021,"に、":-1021,"にし":1771,"にな":1906,"には":2644,"の,":-724,"の、":-724,"の子":-1000,"は,":1337,"は、":1337,"べき":2181,"まし":1113,"ます":6943,"まっ":-1549,"まで":6154,"まれ":-793,"らし":1479,"られ":6820,"るる":3818,"れ,":854,"れ、":854,"れた":1850,"れて":1375,"れば":-3246,"れる":1091,"われ":-605,"んだ":606,"んで":798,"カ月":990,"会議":860,"入り":1232,"大会":2217,"始め":1681,"市":965,"新聞":-5055,"日,":974,"日、":974,"社会":2024,"カ月":990};
|
||||
this.TC1__ = {"AAA":1093,"HHH":1029,"HHM":580,"HII":998,"HOH":-390,"HOM":-331,"IHI":1169,"IOH":-142,"IOI":-1015,"IOM":467,"MMH":187,"OOI":-1832};
|
||||
this.TC2__ = {"HHO":2088,"HII":-1023,"HMM":-1154,"IHI":-1965,"KKH":703,"OII":-2649};
|
||||
this.TC3__ = {"AAA":-294,"HHH":346,"HHI":-341,"HII":-1088,"HIK":731,"HOH":-1486,"IHH":128,"IHI":-3041,"IHO":-1935,"IIH":-825,"IIM":-1035,"IOI":-542,"KHH":-1216,"KKA":491,"KKH":-1217,"KOK":-1009,"MHH":-2694,"MHM":-457,"MHO":123,"MMH":-471,"NNH":-1689,"NNO":662,"OHO":-3393};
|
||||
this.TC4__ = {"HHH":-203,"HHI":1344,"HHK":365,"HHM":-122,"HHN":182,"HHO":669,"HIH":804,"HII":679,"HOH":446,"IHH":695,"IHO":-2324,"IIH":321,"III":1497,"IIO":656,"IOO":54,"KAK":4845,"KKA":3386,"KKK":3065,"MHH":-405,"MHI":201,"MMH":-241,"MMM":661,"MOM":841};
|
||||
this.TQ1__ = {"BHHH":-227,"BHHI":316,"BHIH":-132,"BIHH":60,"BIII":1595,"BNHH":-744,"BOHH":225,"BOOO":-908,"OAKK":482,"OHHH":281,"OHIH":249,"OIHI":200,"OIIH":-68};
|
||||
this.TQ2__ = {"BIHH":-1401,"BIII":-1033,"BKAK":-543,"BOOO":-5591};
|
||||
this.TQ3__ = {"BHHH":478,"BHHM":-1073,"BHIH":222,"BHII":-504,"BIIH":-116,"BIII":-105,"BMHI":-863,"BMHM":-464,"BOMH":620,"OHHH":346,"OHHI":1729,"OHII":997,"OHMH":481,"OIHH":623,"OIIH":1344,"OKAK":2792,"OKHH":587,"OKKA":679,"OOHH":110,"OOII":-685};
|
||||
this.TQ4__ = {"BHHH":-721,"BHHM":-3604,"BHII":-966,"BIIH":-607,"BIII":-2181,"OAAA":-2763,"OAKK":180,"OHHH":-294,"OHHI":2446,"OHHO":480,"OHIH":-1573,"OIHH":1935,"OIHI":-493,"OIIH":626,"OIII":-4007,"OKAK":-8156};
|
||||
this.TW1__ = {"につい":-4681,"東京都":2026};
|
||||
this.TW2__ = {"ある程":-2049,"いった":-1256,"ころが":-2434,"しょう":3873,"その後":-4430,"だって":-1049,"ていた":1833,"として":-4657,"ともに":-4517,"もので":1882,"一気に":-792,"初めて":-1512,"同時に":-8097,"大きな":-1255,"対して":-2721,"社会党":-3216};
|
||||
this.TW3__ = {"いただ":-1734,"してい":1314,"として":-4314,"につい":-5483,"にとっ":-5989,"に当た":-6247,"ので,":-727,"ので、":-727,"のもの":-600,"れから":-3752,"十二月":-2287};
|
||||
this.TW4__ = {"いう.":8576,"いう。":8576,"からな":-2348,"してい":2958,"たが,":1516,"たが、":1516,"ている":1538,"という":1349,"ました":5543,"ません":1097,"ようと":-4258,"よると":5865};
|
||||
this.UC1__ = {"A":484,"K":93,"M":645,"O":-505};
|
||||
this.UC2__ = {"A":819,"H":1059,"I":409,"M":3987,"N":5775,"O":646};
|
||||
this.UC3__ = {"A":-1370,"I":2311};
|
||||
this.UC4__ = {"A":-2643,"H":1809,"I":-1032,"K":-3450,"M":3565,"N":3876,"O":6646};
|
||||
this.UC5__ = {"H":313,"I":-1238,"K":-799,"M":539,"O":-831};
|
||||
this.UC6__ = {"H":-506,"I":-253,"K":87,"M":247,"O":-387};
|
||||
this.UP1__ = {"O":-214};
|
||||
this.UP2__ = {"B":69,"O":935};
|
||||
this.UP3__ = {"B":189};
|
||||
this.UQ1__ = {"BH":21,"BI":-12,"BK":-99,"BN":142,"BO":-56,"OH":-95,"OI":477,"OK":410,"OO":-2422};
|
||||
this.UQ2__ = {"BH":216,"BI":113,"OK":1759};
|
||||
this.UQ3__ = {"BA":-479,"BH":42,"BI":1913,"BK":-7198,"BM":3160,"BN":6427,"BO":14761,"OI":-827,"ON":-3212};
|
||||
this.UW1__ = {",":156,"、":156,"「":-463,"あ":-941,"う":-127,"が":-553,"き":121,"こ":505,"で":-201,"と":-547,"ど":-123,"に":-789,"の":-185,"は":-847,"も":-466,"や":-470,"よ":182,"ら":-292,"り":208,"れ":169,"を":-446,"ん":-137,"・":-135,"主":-402,"京":-268,"区":-912,"午":871,"国":-460,"大":561,"委":729,"市":-411,"日":-141,"理":361,"生":-408,"県":-386,"都":-718,"「":-463,"・":-135};
|
||||
this.UW2__ = {",":-829,"、":-829,"〇":892,"「":-645,"」":3145,"あ":-538,"い":505,"う":134,"お":-502,"か":1454,"が":-856,"く":-412,"こ":1141,"さ":878,"ざ":540,"し":1529,"す":-675,"せ":300,"そ":-1011,"た":188,"だ":1837,"つ":-949,"て":-291,"で":-268,"と":-981,"ど":1273,"な":1063,"に":-1764,"の":130,"は":-409,"ひ":-1273,"べ":1261,"ま":600,"も":-1263,"や":-402,"よ":1639,"り":-579,"る":-694,"れ":571,"を":-2516,"ん":2095,"ア":-587,"カ":306,"キ":568,"ッ":831,"三":-758,"不":-2150,"世":-302,"中":-968,"主":-861,"事":492,"人":-123,"会":978,"保":362,"入":548,"初":-3025,"副":-1566,"北":-3414,"区":-422,"大":-1769,"天":-865,"太":-483,"子":-1519,"学":760,"実":1023,"小":-2009,"市":-813,"年":-1060,"強":1067,"手":-1519,"揺":-1033,"政":1522,"文":-1355,"新":-1682,"日":-1815,"明":-1462,"最":-630,"朝":-1843,"本":-1650,"東":-931,"果":-665,"次":-2378,"民":-180,"気":-1740,"理":752,"発":529,"目":-1584,"相":-242,"県":-1165,"立":-763,"第":810,"米":509,"自":-1353,"行":838,"西":-744,"見":-3874,"調":1010,"議":1198,"込":3041,"開":1758,"間":-1257,"「":-645,"」":3145,"ッ":831,"ア":-587,"カ":306,"キ":568};
|
||||
this.UW3__ = {",":4889,"1":-800,"−":-1723,"、":4889,"々":-2311,"〇":5827,"」":2670,"〓":-3573,"あ":-2696,"い":1006,"う":2342,"え":1983,"お":-4864,"か":-1163,"が":3271,"く":1004,"け":388,"げ":401,"こ":-3552,"ご":-3116,"さ":-1058,"し":-395,"す":584,"せ":3685,"そ":-5228,"た":842,"ち":-521,"っ":-1444,"つ":-1081,"て":6167,"で":2318,"と":1691,"ど":-899,"な":-2788,"に":2745,"の":4056,"は":4555,"ひ":-2171,"ふ":-1798,"へ":1199,"ほ":-5516,"ま":-4384,"み":-120,"め":1205,"も":2323,"や":-788,"よ":-202,"ら":727,"り":649,"る":5905,"れ":2773,"わ":-1207,"を":6620,"ん":-518,"ア":551,"グ":1319,"ス":874,"ッ":-1350,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278,"・":-3794,"一":-1619,"下":-1759,"世":-2087,"両":3815,"中":653,"主":-758,"予":-1193,"二":974,"人":2742,"今":792,"他":1889,"以":-1368,"低":811,"何":4265,"作":-361,"保":-2439,"元":4858,"党":3593,"全":1574,"公":-3030,"六":755,"共":-1880,"円":5807,"再":3095,"分":457,"初":2475,"別":1129,"前":2286,"副":4437,"力":365,"動":-949,"務":-1872,"化":1327,"北":-1038,"区":4646,"千":-2309,"午":-783,"協":-1006,"口":483,"右":1233,"各":3588,"合":-241,"同":3906,"和":-837,"員":4513,"国":642,"型":1389,"場":1219,"外":-241,"妻":2016,"学":-1356,"安":-423,"実":-1008,"家":1078,"小":-513,"少":-3102,"州":1155,"市":3197,"平":-1804,"年":2416,"広":-1030,"府":1605,"度":1452,"建":-2352,"当":-3885,"得":1905,"思":-1291,"性":1822,"戸":-488,"指":-3973,"政":-2013,"教":-1479,"数":3222,"文":-1489,"新":1764,"日":2099,"旧":5792,"昨":-661,"時":-1248,"曜":-951,"最":-937,"月":4125,"期":360,"李":3094,"村":364,"東":-805,"核":5156,"森":2438,"業":484,"氏":2613,"民":-1694,"決":-1073,"法":1868,"海":-495,"無":979,"物":461,"特":-3850,"生":-273,"用":914,"町":1215,"的":7313,"直":-1835,"省":792,"県":6293,"知":-1528,"私":4231,"税":401,"立":-960,"第":1201,"米":7767,"系":3066,"約":3663,"級":1384,"統":-4229,"総":1163,"線":1255,"者":6457,"能":725,"自":-2869,"英":785,"見":1044,"調":-562,"財":-733,"費":1777,"車":1835,"軍":1375,"込":-1504,"通":-1136,"選":-681,"郎":1026,"郡":4404,"部":1200,"金":2163,"長":421,"開":-1432,"間":1302,"関":-1282,"雨":2009,"電":-1045,"非":2066,"駅":1620,"1":-800,"」":2670,"・":-3794,"ッ":-1350,"ア":551,"グ":1319,"ス":874,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278};
|
||||
this.UW4__ = {",":3930,".":3508,"―":-4841,"、":3930,"。":3508,"〇":4999,"「":1895,"」":3798,"〓":-5156,"あ":4752,"い":-3435,"う":-640,"え":-2514,"お":2405,"か":530,"が":6006,"き":-4482,"ぎ":-3821,"く":-3788,"け":-4376,"げ":-4734,"こ":2255,"ご":1979,"さ":2864,"し":-843,"じ":-2506,"す":-731,"ず":1251,"せ":181,"そ":4091,"た":5034,"だ":5408,"ち":-3654,"っ":-5882,"つ":-1659,"て":3994,"で":7410,"と":4547,"な":5433,"に":6499,"ぬ":1853,"ね":1413,"の":7396,"は":8578,"ば":1940,"ひ":4249,"び":-4134,"ふ":1345,"へ":6665,"べ":-744,"ほ":1464,"ま":1051,"み":-2082,"む":-882,"め":-5046,"も":4169,"ゃ":-2666,"や":2795,"ょ":-1544,"よ":3351,"ら":-2922,"り":-9726,"る":-14896,"れ":-2613,"ろ":-4570,"わ":-1783,"を":13150,"ん":-2352,"カ":2145,"コ":1789,"セ":1287,"ッ":-724,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637,"・":-4371,"ー":-11870,"一":-2069,"中":2210,"予":782,"事":-190,"井":-1768,"人":1036,"以":544,"会":950,"体":-1286,"作":530,"側":4292,"先":601,"党":-2006,"共":-1212,"内":584,"円":788,"初":1347,"前":1623,"副":3879,"力":-302,"動":-740,"務":-2715,"化":776,"区":4517,"協":1013,"参":1555,"合":-1834,"和":-681,"員":-910,"器":-851,"回":1500,"国":-619,"園":-1200,"地":866,"場":-1410,"塁":-2094,"士":-1413,"多":1067,"大":571,"子":-4802,"学":-1397,"定":-1057,"寺":-809,"小":1910,"屋":-1328,"山":-1500,"島":-2056,"川":-2667,"市":2771,"年":374,"庁":-4556,"後":456,"性":553,"感":916,"所":-1566,"支":856,"改":787,"政":2182,"教":704,"文":522,"方":-856,"日":1798,"時":1829,"最":845,"月":-9066,"木":-485,"来":-442,"校":-360,"業":-1043,"氏":5388,"民":-2716,"気":-910,"沢":-939,"済":-543,"物":-735,"率":672,"球":-1267,"生":-1286,"産":-1101,"田":-2900,"町":1826,"的":2586,"目":922,"省":-3485,"県":2997,"空":-867,"立":-2112,"第":788,"米":2937,"系":786,"約":2171,"経":1146,"統":-1169,"総":940,"線":-994,"署":749,"者":2145,"能":-730,"般":-852,"行":-792,"規":792,"警":-1184,"議":-244,"谷":-1000,"賞":730,"車":-1481,"軍":1158,"輪":-1433,"込":-3370,"近":929,"道":-1291,"選":2596,"郎":-4866,"都":1192,"野":-1100,"銀":-2213,"長":357,"間":-2344,"院":-2297,"際":-2604,"電":-878,"領":-1659,"題":-792,"館":-1984,"首":1749,"高":2120,"「":1895,"」":3798,"・":-4371,"ッ":-724,"ー":-11870,"カ":2145,"コ":1789,"セ":1287,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637};
|
||||
this.UW5__ = {",":465,".":-299,"1":-514,"E2":-32768,"]":-2762,"、":465,"。":-299,"「":363,"あ":1655,"い":331,"う":-503,"え":1199,"お":527,"か":647,"が":-421,"き":1624,"ぎ":1971,"く":312,"げ":-983,"さ":-1537,"し":-1371,"す":-852,"だ":-1186,"ち":1093,"っ":52,"つ":921,"て":-18,"で":-850,"と":-127,"ど":1682,"な":-787,"に":-1224,"の":-635,"は":-578,"べ":1001,"み":502,"め":865,"ゃ":3350,"ょ":854,"り":-208,"る":429,"れ":504,"わ":419,"を":-1264,"ん":327,"イ":241,"ル":451,"ン":-343,"中":-871,"京":722,"会":-1153,"党":-654,"務":3519,"区":-901,"告":848,"員":2104,"大":-1296,"学":-548,"定":1785,"嵐":-1304,"市":-2991,"席":921,"年":1763,"思":872,"所":-814,"挙":1618,"新":-1682,"日":218,"月":-4353,"査":932,"格":1356,"機":-1508,"氏":-1347,"田":240,"町":-3912,"的":-3149,"相":1319,"省":-1052,"県":-4003,"研":-997,"社":-278,"空":-813,"統":1955,"者":-2233,"表":663,"語":-1073,"議":1219,"選":-1018,"郎":-368,"長":786,"間":1191,"題":2368,"館":-689,"1":-514,"E2":-32768,"「":363,"イ":241,"ル":451,"ン":-343};
|
||||
this.UW6__ = {",":227,".":808,"1":-270,"E1":306,"、":227,"。":808,"あ":-307,"う":189,"か":241,"が":-73,"く":-121,"こ":-200,"じ":1782,"す":383,"た":-428,"っ":573,"て":-1014,"で":101,"と":-105,"な":-253,"に":-149,"の":-417,"は":-236,"も":-206,"り":187,"る":-135,"を":195,"ル":-673,"ン":-496,"一":-277,"中":201,"件":-800,"会":624,"前":302,"区":1792,"員":-1212,"委":798,"学":-960,"市":887,"広":-695,"後":535,"業":-697,"相":753,"社":-507,"福":974,"空":-822,"者":1811,"連":463,"郎":1082,"1":-270,"E1":306,"ル":-673,"ン":-496};
|
||||
|
||||
return this;
|
||||
}
|
||||
TinySegmenter.prototype.ctype_ = function(str) {
|
||||
for (var i in this.chartype_) {
|
||||
if (str.match(this.chartype_[i][0])) {
|
||||
return this.chartype_[i][1];
|
||||
}
|
||||
}
|
||||
return "O";
|
||||
}
|
||||
|
||||
TinySegmenter.prototype.ts_ = function(v) {
|
||||
if (v) { return v; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
TinySegmenter.prototype.segment = function(input) {
|
||||
if (input == null || input == undefined || input == "") {
|
||||
return [];
|
||||
}
|
||||
var result = [];
|
||||
var seg = ["B3","B2","B1"];
|
||||
var ctype = ["O","O","O"];
|
||||
var o = input.split("");
|
||||
for (i = 0; i < o.length; ++i) {
|
||||
seg.push(o[i]);
|
||||
ctype.push(this.ctype_(o[i]))
|
||||
}
|
||||
seg.push("E1");
|
||||
seg.push("E2");
|
||||
seg.push("E3");
|
||||
ctype.push("O");
|
||||
ctype.push("O");
|
||||
ctype.push("O");
|
||||
var word = seg[3];
|
||||
var p1 = "U";
|
||||
var p2 = "U";
|
||||
var p3 = "U";
|
||||
for (var i = 4; i < seg.length - 3; ++i) {
|
||||
var score = this.BIAS__;
|
||||
var w1 = seg[i-3];
|
||||
var w2 = seg[i-2];
|
||||
var w3 = seg[i-1];
|
||||
var w4 = seg[i];
|
||||
var w5 = seg[i+1];
|
||||
var w6 = seg[i+2];
|
||||
var c1 = ctype[i-3];
|
||||
var c2 = ctype[i-2];
|
||||
var c3 = ctype[i-1];
|
||||
var c4 = ctype[i];
|
||||
var c5 = ctype[i+1];
|
||||
var c6 = ctype[i+2];
|
||||
score += this.ts_(this.UP1__[p1]);
|
||||
score += this.ts_(this.UP2__[p2]);
|
||||
score += this.ts_(this.UP3__[p3]);
|
||||
score += this.ts_(this.BP1__[p1 + p2]);
|
||||
score += this.ts_(this.BP2__[p2 + p3]);
|
||||
score += this.ts_(this.UW1__[w1]);
|
||||
score += this.ts_(this.UW2__[w2]);
|
||||
score += this.ts_(this.UW3__[w3]);
|
||||
score += this.ts_(this.UW4__[w4]);
|
||||
score += this.ts_(this.UW5__[w5]);
|
||||
score += this.ts_(this.UW6__[w6]);
|
||||
score += this.ts_(this.BW1__[w2 + w3]);
|
||||
score += this.ts_(this.BW2__[w3 + w4]);
|
||||
score += this.ts_(this.BW3__[w4 + w5]);
|
||||
score += this.ts_(this.TW1__[w1 + w2 + w3]);
|
||||
score += this.ts_(this.TW2__[w2 + w3 + w4]);
|
||||
score += this.ts_(this.TW3__[w3 + w4 + w5]);
|
||||
score += this.ts_(this.TW4__[w4 + w5 + w6]);
|
||||
score += this.ts_(this.UC1__[c1]);
|
||||
score += this.ts_(this.UC2__[c2]);
|
||||
score += this.ts_(this.UC3__[c3]);
|
||||
score += this.ts_(this.UC4__[c4]);
|
||||
score += this.ts_(this.UC5__[c5]);
|
||||
score += this.ts_(this.UC6__[c6]);
|
||||
score += this.ts_(this.BC1__[c2 + c3]);
|
||||
score += this.ts_(this.BC2__[c3 + c4]);
|
||||
score += this.ts_(this.BC3__[c4 + c5]);
|
||||
score += this.ts_(this.TC1__[c1 + c2 + c3]);
|
||||
score += this.ts_(this.TC2__[c2 + c3 + c4]);
|
||||
score += this.ts_(this.TC3__[c3 + c4 + c5]);
|
||||
score += this.ts_(this.TC4__[c4 + c5 + c6]);
|
||||
// score += this.ts_(this.TC5__[c4 + c5 + c6]);
|
||||
score += this.ts_(this.UQ1__[p1 + c1]);
|
||||
score += this.ts_(this.UQ2__[p2 + c2]);
|
||||
score += this.ts_(this.UQ3__[p3 + c3]);
|
||||
score += this.ts_(this.BQ1__[p2 + c2 + c3]);
|
||||
score += this.ts_(this.BQ2__[p2 + c3 + c4]);
|
||||
score += this.ts_(this.BQ3__[p3 + c2 + c3]);
|
||||
score += this.ts_(this.BQ4__[p3 + c3 + c4]);
|
||||
score += this.ts_(this.TQ1__[p2 + c1 + c2 + c3]);
|
||||
score += this.ts_(this.TQ2__[p2 + c2 + c3 + c4]);
|
||||
score += this.ts_(this.TQ3__[p3 + c1 + c2 + c3]);
|
||||
score += this.ts_(this.TQ4__[p3 + c2 + c3 + c4]);
|
||||
var p = "O";
|
||||
if (score > 0) {
|
||||
result.push(word);
|
||||
word = "";
|
||||
p = "B";
|
||||
}
|
||||
p1 = p2;
|
||||
p2 = p3;
|
||||
p3 = p;
|
||||
word += seg[i];
|
||||
}
|
||||
result.push(word);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
lunr.TinySegmenter = TinySegmenter;
|
||||
};
|
||||
|
||||
}));
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
assets/stylesheets/main.484c7ddc.min.css
vendored
1
assets/stylesheets/main.484c7ddc.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
assets/stylesheets/palette.ab4e12ef.min.css
vendored
1
assets/stylesheets/palette.ab4e12ef.min.css
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"sources":["src/templates/assets/stylesheets/palette/_scheme.scss","../../../../src/templates/assets/stylesheets/palette.scss","src/templates/assets/stylesheets/palette/_accent.scss","src/templates/assets/stylesheets/palette/_primary.scss","src/templates/assets/stylesheets/utilities/_break.scss"],"names":[],"mappings":"AA2BA,cAGE,6BAME,sDAAA,CACA,6DAAA,CACA,+DAAA,CACA,gEAAA,CACA,mDAAA,CACA,6DAAA,CACA,+DAAA,CACA,gEAAA,CAGA,mDAAA,CACA,gDAAA,CACA,yDAAA,CACA,4DAAA,CAGA,0BAAA,CACA,mCAAA,CAGA,iCAAA,CACA,kCAAA,CACA,mCAAA,CACA,mCAAA,CACA,kCAAA,CACA,iCAAA,CACA,+CAAA,CACA,6DAAA,CACA,gEAAA,CACA,4DAAA,CACA,4DAAA,CACA,6DAAA,CAGA,6CAAA,CAGA,+CAAA,CAGA,uDAAA,CACA,6DAAA,CACA,2DAAA,CAGA,iCAAA,CAGA,yDAAA,CACA,iEAAA,CAGA,mDAAA,CACA,mDAAA,CAGA,qDAAA,CACA,uDAAA,CAGA,8DAAA,CAKA,8DAAA,CAKA,0DAAA,CAzEA,iBCiBF,CD6DE,kHAEE,YC3DJ,CDkFE,yDACE,4BChFJ,CD+EE,2DACE,4BC7EJ,CD4EE,gEACE,4BC1EJ,CDyEE,2DACE,4BCvEJ,CDsEE,yDACE,4BCpEJ,CDmEE,0DACE,4BCjEJ,CDgEE,gEACE,4BC9DJ,CD6DE,0DACE,4BC3DJ,CD0DE,2OACE,4BC/CJ,CDsDA,+FAGE,iCCpDF,CACF,CCjDE,2BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD6CN,CCvDE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDoDN,CC9DE,8BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD2DN,CCrEE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDkEN,CC5EE,8BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDyEN,CCnFE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDgFN,CC1FE,kCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDuFN,CCjGE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD8FN,CCxGE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDqGN,CC/GE,6BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD4GN,CCtHE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDmHN,CC7HE,4BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCD6HN,CCpIE,8BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCDoIN,CC3IE,6BACE,yBAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCD2IN,CClJE,8BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCDkJN,CCzJE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDsJN,CE3JE,4BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwJN,CEnKE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgKN,CE3KE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwKN,CEnLE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgLN,CE3LE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwLN,CEnME,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgMN,CE3ME,mCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwMN,CEnNE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgNN,CE3NE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwNN,CEnOE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgON,CE3OE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwON,CEnPE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCFmPN,CE3PE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCF2PN,CEnQE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCFmQN,CE3QE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCF2QN,CEnRE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgRN,CE3RE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwRN,CEnSE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCAAA,CAKA,4BF4RN,CE5SE,kCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCAAA,CAKA,4BFqSN,CEtRE,sEACE,4BFyRJ,CE1RE,+DACE,4BF6RJ,CE9RE,iEACE,4BFiSJ,CElSE,gEACE,4BFqSJ,CEtSE,iEACE,4BFySJ,CEhSA,8BACE,mDAAA,CACA,4DAAA,CACA,0DAAA,CACA,oDAAA,CACA,2DAAA,CAGA,4BFiSF,CE9RE,yCACE,+BFgSJ,CE7RI,kDAEE,0CAAA,CACA,sCAAA,CAFA,mCFiSN,CG7MI,mCD1EA,+CACE,8CF0RJ,CEvRI,qDACE,8CFyRN,CEpRE,iEACE,mCFsRJ,CACF,CGxNI,sCDvDA,uCACE,oCFkRJ,CACF,CEzQA,8BACE,kDAAA,CACA,4DAAA,CACA,wDAAA,CACA,oDAAA,CACA,6DAAA,CAGA,4BF0QF,CEvQE,yCACE,+BFyQJ,CEtQI,kDAEE,0CAAA,CACA,sCAAA,CAFA,mCF0QN,CEnQE,yCACE,6CFqQJ,CG9NI,0CDhCA,8CACE,gDFiQJ,CACF,CGnOI,0CDvBA,iFACE,6CF6PJ,CACF,CG3PI,sCDKA,uCACE,6CFyPJ,CACF","file":"palette.css"}
|
||||
839
cli/index.html
839
cli/index.html
|
|
@ -1,839 +0,0 @@
|
|||
|
||||
<!doctype html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<meta name="description" content="CLI and library for collecting repositories, websites, and PWAs into portable data artifacts.">
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="prev" href="../installation/">
|
||||
|
||||
|
||||
<link rel="next" href="../library/">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="icon" href="../assets/images/favicon.png">
|
||||
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.1">
|
||||
|
||||
|
||||
|
||||
<title>CLI Usage - Borg Data Collector</title>
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../assets/stylesheets/main.484c7ddc.min.css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../assets/stylesheets/palette.ab4e12ef.min.css">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
|
||||
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
|
||||
|
||||
|
||||
|
||||
<script>__md_scope=new URL("..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<body dir="ltr" data-md-color-scheme="default" data-md-color-primary="blue" data-md-color-accent="indigo">
|
||||
|
||||
|
||||
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
|
||||
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
|
||||
<label class="md-overlay" for="__drawer"></label>
|
||||
<div data-md-component="skip">
|
||||
|
||||
|
||||
<a href="#cli-usage" class="md-skip">
|
||||
Skip to content
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<div data-md-component="announce">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<header class="md-header" data-md-component="header">
|
||||
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||
<a href=".." title="Borg Data Collector" class="md-header__button md-logo" aria-label="Borg Data Collector" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
|
||||
</a>
|
||||
<label class="md-header__button md-icon" for="__drawer">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||
</label>
|
||||
<div class="md-header__title" data-md-component="header-title">
|
||||
<div class="md-header__ellipsis">
|
||||
<div class="md-header__topic">
|
||||
<span class="md-ellipsis">
|
||||
Borg Data Collector
|
||||
</span>
|
||||
</div>
|
||||
<div class="md-header__topic" data-md-component="header-topic">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
CLI Usage
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form class="md-header__option" data-md-component="palette">
|
||||
|
||||
|
||||
|
||||
|
||||
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="blue" data-md-color-accent="indigo" aria-hidden="true" type="radio" name="__palette" id="__palette_0">
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-header__button md-icon" for="__search">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||
</label>
|
||||
<div class="md-search" data-md-component="search" role="dialog">
|
||||
<label class="md-search__overlay" for="__search"></label>
|
||||
<div class="md-search__inner" role="search">
|
||||
<form class="md-search__form" name="search">
|
||||
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
|
||||
<label class="md-search__icon md-icon" for="__search">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
|
||||
</label>
|
||||
<nav class="md-search__options" aria-label="Search">
|
||||
|
||||
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
</form>
|
||||
<div class="md-search__output">
|
||||
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
|
||||
<div class="md-search-result" data-md-component="search-result">
|
||||
<div class="md-search-result__meta">
|
||||
Initializing search
|
||||
</div>
|
||||
<ol class="md-search-result__list" role="presentation"></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="md-header__source">
|
||||
<a href="https://github.com/Snider/Borg" title="Go to repository" class="md-source" data-md-component="source">
|
||||
<div class="md-source__icon md-icon">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||
</div>
|
||||
<div class="md-source__repository">
|
||||
GitHub
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="md-container" data-md-component="container">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||
<div class="md-grid">
|
||||
<ul class="md-tabs__list">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href=".." class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Overview
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="../installation/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Installation
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item md-tabs__item--active">
|
||||
<a href="./" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
CLI Usage
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="../library/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Library Usage
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="../development/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Development
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="../releasing/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Releasing
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<main class="md-main" data-md-component="main">
|
||||
<div class="md-main__inner md-grid">
|
||||
|
||||
|
||||
|
||||
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--primary md-nav--lifted md-nav--integrated" aria-label="Navigation" data-md-level="0">
|
||||
<label class="md-nav__title" for="__drawer">
|
||||
<a href=".." title="Borg Data Collector" class="md-nav__button md-logo" aria-label="Borg Data Collector" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
|
||||
</a>
|
||||
Borg Data Collector
|
||||
</label>
|
||||
|
||||
<div class="md-nav__source">
|
||||
<a href="https://github.com/Snider/Borg" title="Go to repository" class="md-source" data-md-component="source">
|
||||
<div class="md-source__icon md-icon">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||
</div>
|
||||
<div class="md-source__repository">
|
||||
GitHub
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href=".." class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
Overview
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../installation/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
Installation
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item md-nav__item--active">
|
||||
|
||||
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__link md-nav__link--active" for="__toc">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
CLI Usage
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
</label>
|
||||
|
||||
<a href="./" class="md-nav__link md-nav__link--active">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
CLI Usage
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__title" for="__toc">
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
Table of contents
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#top-level" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Top-level
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#commands" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Commands
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="Commands">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#collect" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
collect
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#all" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
all
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#compile" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
compile
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#run" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
run
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#serve" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
serve
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#decode" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
decode
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#compression" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Compression
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#formats" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Formats
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../library/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
Library Usage
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../development/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
Development
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../releasing/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
Releasing
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="md-content" data-md-component="content">
|
||||
|
||||
<article class="md-content__inner md-typeset">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h1 id="cli-usage">CLI Usage</h1>
|
||||
<p><code>borg</code> is a command-line tool for collecting repositories, websites, and PWAs into portable data artifacts (DataNodes) or Terminal Isolation Matrices.</p>
|
||||
<p>Use <code>borg --help</code> and <code>borg <command> --help</code> to see all flags.</p>
|
||||
<h2 id="top-level">Top-level</h2>
|
||||
<ul>
|
||||
<li><code>borg --help</code></li>
|
||||
<li><code>borg --version</code></li>
|
||||
</ul>
|
||||
<h2 id="commands">Commands</h2>
|
||||
<h3 id="collect">collect</h3>
|
||||
<p>Collect and package inputs.</p>
|
||||
<p>Subcommands:
|
||||
- <code>borg collect github repo <repo-url> [--output <file>] [--format datanode|tim|trix] [--compression none|gz|xz]</code>
|
||||
- <code>borg collect github release <release-url> [--output <file>]</code>
|
||||
- <code>borg collect github repos <org-or-user> [--output <file>] [--format ...] [--compression ...]</code>
|
||||
- <code>borg collect website <url> [--depth N] [--output <file>] [--format ...] [--compression ...]</code>
|
||||
- <code>borg collect pwa --uri <url> [--output <file>] [--format ...] [--compression ...]</code></p>
|
||||
<p>Examples:
|
||||
- <code>borg collect github repo https://github.com/Snider/Borg --output borg.dat</code>
|
||||
- <code>borg collect website https://example.com --depth 1 --output site.dat</code>
|
||||
- <code>borg collect pwa --uri https://squoosh.app --output squoosh.dat</code></p>
|
||||
<h3 id="all">all</h3>
|
||||
<p>Collect all public repositories from a GitHub user or organization.</p>
|
||||
<ul>
|
||||
<li><code>borg all <url> [--output <file>]</code></li>
|
||||
</ul>
|
||||
<p>Example:
|
||||
- <code>borg all https://github.com/Snider --output snider.dat</code></p>
|
||||
<h3 id="compile">compile</h3>
|
||||
<p>Compile a Borgfile into a Terminal Isolation Matrix (TIM).</p>
|
||||
<ul>
|
||||
<li><code>borg compile [--file <Borgfile>] [--output <file>]</code></li>
|
||||
</ul>
|
||||
<p>Example:
|
||||
- <code>borg compile --file Borgfile --output a.tim</code></p>
|
||||
<h3 id="run">run</h3>
|
||||
<p>Execute a Terminal Isolation Matrix (TIM).</p>
|
||||
<ul>
|
||||
<li><code>borg run <tim-file></code></li>
|
||||
</ul>
|
||||
<p>Example:
|
||||
- <code>borg run a.tim</code></p>
|
||||
<h3 id="serve">serve</h3>
|
||||
<p>Serve a packaged DataNode or TIM via a static file server.</p>
|
||||
<ul>
|
||||
<li><code>borg serve <file> [--port 8080]</code></li>
|
||||
</ul>
|
||||
<p>Examples:
|
||||
- <code>borg serve squoosh.dat --port 8888</code>
|
||||
- <code>borg serve borg.tim --port 9999</code></p>
|
||||
<h3 id="decode">decode</h3>
|
||||
<p>Decode a <code>.trix</code> or <code>.tim</code> file back into a DataNode (<code>.dat</code>).</p>
|
||||
<ul>
|
||||
<li><code>borg decode <file> [--output <file>] [--password <password>]</code></li>
|
||||
</ul>
|
||||
<p>Examples:
|
||||
- <code>borg decode borg.trix --output borg.dat --password "secret"</code>
|
||||
- <code>borg decode borg.tim --output borg.dat --i-am-in-isolation</code></p>
|
||||
<h2 id="compression">Compression</h2>
|
||||
<p>All collect commands accept <code>--compression</code> with values:
|
||||
- <code>none</code> (default)
|
||||
- <code>gz</code>
|
||||
- <code>xz</code></p>
|
||||
<p>Output filenames gain the appropriate extension automatically.</p>
|
||||
<h2 id="formats">Formats</h2>
|
||||
<p>Borg supports three output formats via the <code>--format</code> flag:</p>
|
||||
<ul>
|
||||
<li><code>datanode</code>: A simple tarball containing the collected resources. (Default)</li>
|
||||
<li><code>tim</code>: Terminal Isolation Matrix, a runc-compatible bundle.</li>
|
||||
<li><code>trix</code>: Encrypted and structured file format.</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
||||
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="md-footer">
|
||||
|
||||
<div class="md-footer-meta md-typeset">
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
<div class="md-copyright">
|
||||
|
||||
|
||||
Made with
|
||||
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
|
||||
Material for MkDocs
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<div class="md-dialog" data-md-component="dialog">
|
||||
<div class="md-dialog__inner md-typeset"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script id="__config" type="application/json">{"annotate": null, "base": "..", "features": ["navigation.tabs", "navigation.sections", "content.code.copy", "toc.integrate"], "search": "../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
|
||||
|
||||
|
||||
<script src="../assets/javascripts/bundle.79ae519e.min.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
178
cmd/all.go
Normal file
178
cmd/all.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/Snider/Borg/pkg/github"
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
"github.com/Snider/Borg/pkg/ui"
|
||||
"github.com/Snider/Borg/pkg/vcs"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var allCmd = NewAllCmd()
|
||||
|
||||
func NewAllCmd() *cobra.Command {
|
||||
allCmd := &cobra.Command{
|
||||
Use: "all [url]",
|
||||
Short: "Collect all resources from a URL",
|
||||
Long: `Collect all resources from a URL, dispatching to the appropriate collector based on the URL type.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
url := args[0]
|
||||
outputFile, _ := cmd.Flags().GetString("output")
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
compression, _ := cmd.Flags().GetString("compression")
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
|
||||
if format != "datanode" && format != "tim" && format != "trix" {
|
||||
return fmt.Errorf("invalid format: %s (must be 'datanode', 'tim', or 'trix')", format)
|
||||
}
|
||||
|
||||
owner, err := parseGithubOwner(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repos, err := GithubClient.GetPublicRepos(cmd.Context(), owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prompter := ui.NewNonInteractivePrompter(ui.GetVCSQuote)
|
||||
prompter.Start()
|
||||
defer prompter.Stop()
|
||||
|
||||
var progressWriter io.Writer
|
||||
if prompter.IsInteractive() {
|
||||
bar := ui.NewProgressBar(len(repos), "Cloning repositories")
|
||||
progressWriter = ui.NewProgressWriter(bar)
|
||||
}
|
||||
|
||||
cloner := vcs.NewGitCloner()
|
||||
allDataNodes := datanode.New()
|
||||
|
||||
for _, repoURL := range repos {
|
||||
dn, err := cloner.CloneGitRepository(repoURL, progressWriter)
|
||||
if err != nil {
|
||||
// Log the error and continue
|
||||
fmt.Fprintln(cmd.ErrOrStderr(), "Error cloning repository:", err)
|
||||
continue
|
||||
}
|
||||
// This is not an efficient way to merge datanodes, but it's the only way for now
|
||||
// A better approach would be to add a Merge method to the DataNode
|
||||
repoName := strings.TrimSuffix(repoURL, ".git")
|
||||
parts := strings.Split(repoName, "/")
|
||||
repoName = parts[len(parts)-1]
|
||||
|
||||
err = dn.Walk(".", func(path string, de fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !de.IsDir() {
|
||||
err := func() error {
|
||||
file, err := dn.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allDataNodes.AddData(repoName+"/"+path, data)
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(cmd.ErrOrStderr(), "Error walking datanode:", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if format == "tim" {
|
||||
tim, err := tim.FromDataNode(allDataNodes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating tim: %w", err)
|
||||
}
|
||||
data, err = tim.ToTar()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing tim: %w", err)
|
||||
}
|
||||
} else if format == "trix" {
|
||||
data, err = trix.ToTrix(allDataNodes, password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing trix: %w", err)
|
||||
}
|
||||
} else {
|
||||
data, err = allDataNodes.ToTar()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing DataNode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
compressedData, err := compress.Compress(data, compression)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error compressing data: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(outputFile, compressedData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing DataNode to file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "All repositories saved to", outputFile)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
allCmd.PersistentFlags().String("output", "all.dat", "Output file for the DataNode")
|
||||
allCmd.PersistentFlags().String("format", "datanode", "Output format (datanode, tim, or trix)")
|
||||
allCmd.PersistentFlags().String("compression", "none", "Compression format (none, gz, or xz)")
|
||||
allCmd.PersistentFlags().String("password", "", "Password for encryption")
|
||||
return allCmd
|
||||
}
|
||||
|
||||
func GetAllCmd() *cobra.Command {
|
||||
return allCmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(GetAllCmd())
|
||||
}
|
||||
|
||||
func parseGithubOwner(u string) (string, error) {
|
||||
owner, _, err := github.ParseRepoFromURL(u)
|
||||
if err == nil {
|
||||
return owner, nil
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
|
||||
path := strings.Trim(parsedURL.Path, "/")
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("invalid owner URL: %s", u)
|
||||
}
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) != 1 || parts[0] == "" {
|
||||
return "", fmt.Errorf("invalid owner URL: %s", u)
|
||||
}
|
||||
return parts[0], nil
|
||||
}
|
||||
116
cmd/all_test.go
Normal file
116
cmd/all_test.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/Snider/Borg/pkg/github"
|
||||
"github.com/Snider/Borg/pkg/mocks"
|
||||
)
|
||||
|
||||
func TestAllCmd_Good(t *testing.T) {
|
||||
// Setup mock HTTP client for GitHub API
|
||||
mockGithubClient := mocks.NewMockClient(map[string]*http.Response{
|
||||
"https://api.github.com/users/testuser/repos": {
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[{"clone_url": "https://github.com/testuser/repo1.git"}]`)),
|
||||
},
|
||||
})
|
||||
oldNewAuthenticatedClient := github.NewAuthenticatedClient
|
||||
github.NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
||||
return mockGithubClient
|
||||
}
|
||||
defer func() {
|
||||
github.NewAuthenticatedClient = oldNewAuthenticatedClient
|
||||
}()
|
||||
|
||||
// Setup mock Git cloner
|
||||
mockCloner := &mocks.MockGitCloner{
|
||||
DN: datanode.New(),
|
||||
Err: nil,
|
||||
}
|
||||
oldCloner := GitCloner
|
||||
GitCloner = mockCloner
|
||||
defer func() {
|
||||
GitCloner = oldCloner
|
||||
}()
|
||||
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetAllCmd())
|
||||
|
||||
// Execute command
|
||||
out := filepath.Join(t.TempDir(), "out")
|
||||
_, err := executeCommand(rootCmd, "all", "https://github.com/testuser", "--output", out)
|
||||
if err != nil {
|
||||
t.Fatalf("all command failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllCmd_Bad(t *testing.T) {
|
||||
// Setup mock HTTP client to return an error
|
||||
mockGithubClient := mocks.NewMockClient(map[string]*http.Response{
|
||||
"https://api.github.com/users/baduser/repos": {
|
||||
StatusCode: http.StatusNotFound,
|
||||
Status: "404 Not Found",
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"message": "Not Found"}`)),
|
||||
},
|
||||
"https://api.github.com/orgs/baduser/repos": {
|
||||
StatusCode: http.StatusNotFound,
|
||||
Status: "404 Not Found",
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"message": "Not Found"}`)),
|
||||
},
|
||||
})
|
||||
oldNewAuthenticatedClient := github.NewAuthenticatedClient
|
||||
github.NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
||||
return mockGithubClient
|
||||
}
|
||||
defer func() {
|
||||
github.NewAuthenticatedClient = oldNewAuthenticatedClient
|
||||
}()
|
||||
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetAllCmd())
|
||||
|
||||
// Execute command
|
||||
out := filepath.Join(t.TempDir(), "out")
|
||||
_, err := executeCommand(rootCmd, "all", "https://github.com/baduser", "--output", out)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllCmd_Ugly(t *testing.T) {
|
||||
t.Run("User with no repos", func(t *testing.T) {
|
||||
// Setup mock HTTP client for a user with no repos
|
||||
mockGithubClient := mocks.NewMockClient(map[string]*http.Response{
|
||||
"https://api.github.com/users/emptyuser/repos": {
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[]`)),
|
||||
},
|
||||
})
|
||||
oldNewAuthenticatedClient := github.NewAuthenticatedClient
|
||||
github.NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
||||
return mockGithubClient
|
||||
}
|
||||
defer func() {
|
||||
github.NewAuthenticatedClient = oldNewAuthenticatedClient
|
||||
}()
|
||||
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetAllCmd())
|
||||
|
||||
// Execute command
|
||||
out := filepath.Join(t.TempDir(), "out")
|
||||
_, err := executeCommand(rootCmd, "all", "https://github.com/emptyuser", "--output", out)
|
||||
if err != nil {
|
||||
t.Fatalf("all command failed for user with no repos: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
23
cmd/collect.go
Normal file
23
cmd/collect.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// collectCmd represents the collect command
|
||||
var collectCmd = NewCollectCmd()
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(GetCollectCmd())
|
||||
}
|
||||
func NewCollectCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "collect",
|
||||
Short: "Collect a resource from a URI.",
|
||||
Long: `Collect a resource from a URI and store it in a DataNode.`,
|
||||
}
|
||||
}
|
||||
|
||||
func GetCollectCmd() *cobra.Command {
|
||||
return collectCmd
|
||||
}
|
||||
19
cmd/collect_github.go
Normal file
19
cmd/collect_github.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// collectGithubCmd represents the collect github command
|
||||
var collectGithubCmd = &cobra.Command{
|
||||
Use: "github",
|
||||
Short: "Collect a resource from GitHub.",
|
||||
Long: `Collect a resource from a GitHub repository, such as a repository or a release.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
collectCmd.AddCommand(collectGithubCmd)
|
||||
}
|
||||
func GetCollectGithubCmd() *cobra.Command {
|
||||
return collectGithubCmd
|
||||
}
|
||||
151
cmd/collect_github_release_subcommand.go
Normal file
151
cmd/collect_github_release_subcommand.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
borg_github "github.com/Snider/Borg/pkg/github"
|
||||
"github.com/google/go-github/v39/github"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
func NewCollectGithubReleaseCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "release [repository-url]",
|
||||
Short: "Download the latest release of a file from GitHub releases",
|
||||
Long: `Download the latest release of a file from GitHub releases. If the file or URL has a version number, it will check for a higher version and download it if found.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logVal := cmd.Context().Value("logger")
|
||||
log, ok := logVal.(*slog.Logger)
|
||||
if !ok || log == nil {
|
||||
return errors.New("logger not properly initialised")
|
||||
}
|
||||
repoURL := args[0]
|
||||
outputDir, _ := cmd.Flags().GetString("output")
|
||||
pack, _ := cmd.Flags().GetBool("pack")
|
||||
file, _ := cmd.Flags().GetString("file")
|
||||
version, _ := cmd.Flags().GetString("version")
|
||||
|
||||
_, err := GetRelease(log, repoURL, outputDir, pack, file, version)
|
||||
return err
|
||||
},
|
||||
}
|
||||
cmd.PersistentFlags().String("output", ".", "Output directory for the downloaded file")
|
||||
cmd.PersistentFlags().Bool("pack", false, "Pack all assets into a DataNode")
|
||||
cmd.PersistentFlags().String("file", "", "The file to download from the release")
|
||||
cmd.PersistentFlags().String("version", "", "The version to check against")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
collectGithubCmd.AddCommand(NewCollectGithubReleaseCmd())
|
||||
}
|
||||
|
||||
func GetRelease(log *slog.Logger, repoURL string, outputDir string, pack bool, file string, version string) (*github.RepositoryRelease, error) {
|
||||
owner, repo, err := borg_github.ParseRepoFromURL(repoURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse repository url: %w", err)
|
||||
}
|
||||
|
||||
release, err := borg_github.GetLatestRelease(owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get latest release: %w", err)
|
||||
}
|
||||
|
||||
log.Info("found latest release", "tag", release.GetTagName())
|
||||
|
||||
if version != "" {
|
||||
tag := release.GetTagName()
|
||||
if !semver.IsValid(tag) {
|
||||
log.Info("latest release tag is not a valid semantic version, skipping comparison", "tag", tag)
|
||||
} else {
|
||||
if !semver.IsValid(version) {
|
||||
return nil, fmt.Errorf("invalid version string: %s", version)
|
||||
}
|
||||
if semver.Compare(tag, version) <= 0 {
|
||||
log.Info("latest release is not newer than the provided version", "latest", tag, "provided", version)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pack {
|
||||
dn := datanode.New()
|
||||
var failedAssets []string
|
||||
for _, asset := range release.Assets {
|
||||
log.Info("downloading asset", "name", asset.GetName())
|
||||
data, err := borg_github.DownloadReleaseAsset(asset)
|
||||
if err != nil {
|
||||
log.Error("failed to download asset", "name", asset.GetName(), "err", err)
|
||||
failedAssets = append(failedAssets, asset.GetName())
|
||||
continue
|
||||
}
|
||||
dn.AddData(asset.GetName(), data)
|
||||
}
|
||||
if len(failedAssets) > 0 {
|
||||
return nil, fmt.Errorf("failed to download assets: %v", failedAssets)
|
||||
}
|
||||
|
||||
tar, err := dn.ToTar()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create datanode: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
basename := release.GetTagName()
|
||||
if basename == "" {
|
||||
basename = "release"
|
||||
}
|
||||
outputFile := filepath.Join(outputDir, basename+".dat")
|
||||
|
||||
err = os.WriteFile(outputFile, tar, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write datanode: %w", err)
|
||||
}
|
||||
log.Info("datanode saved", "path", outputFile)
|
||||
} else {
|
||||
if len(release.Assets) == 0 {
|
||||
log.Info("no assets found in the latest release")
|
||||
return nil, nil
|
||||
}
|
||||
var assetToDownload *github.ReleaseAsset
|
||||
if file != "" {
|
||||
for _, asset := range release.Assets {
|
||||
if asset.GetName() == file {
|
||||
assetToDownload = asset
|
||||
break
|
||||
}
|
||||
}
|
||||
if assetToDownload == nil {
|
||||
return nil, fmt.Errorf("asset not found in the latest release: %s", file)
|
||||
}
|
||||
} else {
|
||||
assetToDownload = release.Assets[0]
|
||||
}
|
||||
if outputDir != "" {
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
}
|
||||
outputPath := filepath.Join(outputDir, assetToDownload.GetName())
|
||||
log.Info("downloading asset", "name", assetToDownload.GetName())
|
||||
data, err := borg_github.DownloadReleaseAsset(assetToDownload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download asset: %w", err)
|
||||
}
|
||||
err = os.WriteFile(outputPath, data, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write asset to file: %w", err)
|
||||
}
|
||||
log.Info("asset downloaded", "path", outputPath)
|
||||
}
|
||||
return release, nil
|
||||
}
|
||||
126
cmd/collect_github_repo.go
Normal file
126
cmd/collect_github_repo.go
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
"github.com/Snider/Borg/pkg/ui"
|
||||
"github.com/Snider/Borg/pkg/vcs"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFilePermission = 0644
|
||||
)
|
||||
|
||||
var (
|
||||
// GitCloner is the git cloner used by the command. It can be replaced for testing.
|
||||
GitCloner = vcs.NewGitCloner()
|
||||
)
|
||||
|
||||
// NewCollectGithubRepoCmd creates a new cobra command for collecting a single git repository.
|
||||
func NewCollectGithubRepoCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "repo [repository-url]",
|
||||
Short: "Collect a single Git repository",
|
||||
Long: `Collect a single Git repository and store it in a DataNode.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
repoURL := args[0]
|
||||
outputFile, _ := cmd.Flags().GetString("output")
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
compression, _ := cmd.Flags().GetString("compression")
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
|
||||
if format != "datanode" && format != "tim" && format != "trix" && format != "stim" {
|
||||
return fmt.Errorf("invalid format: %s (must be 'datanode', 'tim', 'trix', or 'stim')", format)
|
||||
}
|
||||
if compression != "none" && compression != "gz" && compression != "xz" {
|
||||
return fmt.Errorf("invalid compression: %s (must be 'none', 'gz', or 'xz')", compression)
|
||||
}
|
||||
|
||||
prompter := ui.NewNonInteractivePrompter(ui.GetVCSQuote)
|
||||
prompter.Start()
|
||||
defer prompter.Stop()
|
||||
|
||||
var progressWriter io.Writer
|
||||
if prompter.IsInteractive() {
|
||||
bar := ui.NewProgressBar(-1, "Cloning repository")
|
||||
progressWriter = ui.NewProgressWriter(bar)
|
||||
}
|
||||
|
||||
dn, err := GitCloner.CloneGitRepository(repoURL, progressWriter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error cloning repository: %w", err)
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if format == "tim" {
|
||||
t, err := tim.FromDataNode(dn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating tim: %w", err)
|
||||
}
|
||||
data, err = t.ToTar()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing tim: %w", err)
|
||||
}
|
||||
} else if format == "stim" {
|
||||
if password == "" {
|
||||
return fmt.Errorf("password required for stim format")
|
||||
}
|
||||
t, err := tim.FromDataNode(dn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating tim: %w", err)
|
||||
}
|
||||
data, err = t.ToSigil(password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error encrypting stim: %w", err)
|
||||
}
|
||||
} else if format == "trix" {
|
||||
data, err = trix.ToTrix(dn, password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing trix: %w", err)
|
||||
}
|
||||
} else {
|
||||
data, err = dn.ToTar()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing DataNode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
compressedData, err := compress.Compress(data, compression)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error compressing data: %w", err)
|
||||
}
|
||||
|
||||
if outputFile == "" {
|
||||
outputFile = "repo." + format
|
||||
if compression != "none" {
|
||||
outputFile += "." + compression
|
||||
}
|
||||
}
|
||||
|
||||
err = os.WriteFile(outputFile, compressedData, defaultFilePermission)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing DataNode to file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "Repository saved to", outputFile)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().String("output", "", "Output file for the DataNode")
|
||||
cmd.Flags().String("format", "datanode", "Output format (datanode, tim, trix, or stim)")
|
||||
cmd.Flags().String("compression", "none", "Compression format (none, gz, or xz)")
|
||||
cmd.Flags().String("password", "", "Password for encryption (required for trix/stim)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
collectGithubCmd.AddCommand(NewCollectGithubRepoCmd())
|
||||
}
|
||||
67
cmd/collect_github_repo_test.go
Normal file
67
cmd/collect_github_repo_test.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/Snider/Borg/pkg/mocks"
|
||||
)
|
||||
|
||||
func TestCollectGithubRepoCmd_Good(t *testing.T) {
|
||||
// Setup mock Git cloner
|
||||
mockCloner := &mocks.MockGitCloner{
|
||||
DN: datanode.New(),
|
||||
Err: nil,
|
||||
}
|
||||
oldCloner := GitCloner
|
||||
GitCloner = mockCloner
|
||||
defer func() {
|
||||
GitCloner = oldCloner
|
||||
}()
|
||||
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetCollectCmd())
|
||||
|
||||
// Execute command
|
||||
out := filepath.Join(t.TempDir(), "out")
|
||||
_, err := executeCommand(rootCmd, "collect", "github", "repo", "https://github.com/testuser/repo1", "--output", out)
|
||||
if err != nil {
|
||||
t.Fatalf("collect github repo command failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectGithubRepoCmd_Bad(t *testing.T) {
|
||||
// Setup mock Git cloner to return an error
|
||||
mockCloner := &mocks.MockGitCloner{
|
||||
DN: nil,
|
||||
Err: fmt.Errorf("git clone error"),
|
||||
}
|
||||
oldCloner := GitCloner
|
||||
GitCloner = mockCloner
|
||||
defer func() {
|
||||
GitCloner = oldCloner
|
||||
}()
|
||||
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetCollectCmd())
|
||||
|
||||
// Execute command
|
||||
out := filepath.Join(t.TempDir(), "out")
|
||||
_, err := executeCommand(rootCmd, "collect", "github", "repo", "https://github.com/testuser/repo1", "--output", out)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectGithubRepoCmd_Ugly(t *testing.T) {
|
||||
t.Run("Invalid repo URL", func(t *testing.T) {
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetCollectCmd())
|
||||
_, err := executeCommand(rootCmd, "collect", "github", "repo", "not-a-github-url")
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for invalid repo URL, but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
33
cmd/collect_github_repos.go
Normal file
33
cmd/collect_github_repos.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Snider/Borg/pkg/github"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// GithubClient is the github client used by the command. It can be replaced for testing.
|
||||
GithubClient = github.NewGithubClient()
|
||||
)
|
||||
|
||||
var collectGithubReposCmd = &cobra.Command{
|
||||
Use: "repos [user-or-org]",
|
||||
Short: "Collects all public repositories for a user or organization",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
repos, err := GithubClient.GetPublicRepos(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, repo := range repos {
|
||||
fmt.Fprintln(cmd.OutOrStdout(), repo)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
collectGithubCmd.AddCommand(collectGithubReposCmd)
|
||||
}
|
||||
333
cmd/collect_local.go
Normal file
333
cmd/collect_local.go
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
"github.com/Snider/Borg/pkg/ui"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CollectLocalCmd struct {
|
||||
cobra.Command
|
||||
}
|
||||
|
||||
// NewCollectLocalCmd creates a new collect local command
|
||||
func NewCollectLocalCmd() *CollectLocalCmd {
|
||||
c := &CollectLocalCmd{}
|
||||
c.Command = cobra.Command{
|
||||
Use: "local [directory]",
|
||||
Short: "Collect files from a local directory",
|
||||
Long: `Collect files from a local directory and store them in a DataNode.
|
||||
|
||||
If no directory is specified, the current working directory is used.
|
||||
|
||||
Examples:
|
||||
borg collect local
|
||||
borg collect local ./src
|
||||
borg collect local /path/to/project --output project.tar
|
||||
borg collect local . --format stim --password secret
|
||||
borg collect local . --exclude "*.log" --exclude "node_modules"`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
directory := "."
|
||||
if len(args) > 0 {
|
||||
directory = args[0]
|
||||
}
|
||||
|
||||
outputFile, _ := cmd.Flags().GetString("output")
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
compression, _ := cmd.Flags().GetString("compression")
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
excludes, _ := cmd.Flags().GetStringSlice("exclude")
|
||||
includeHidden, _ := cmd.Flags().GetBool("hidden")
|
||||
respectGitignore, _ := cmd.Flags().GetBool("gitignore")
|
||||
|
||||
finalPath, err := CollectLocal(directory, outputFile, format, compression, password, excludes, includeHidden, respectGitignore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "Files saved to", finalPath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
c.Flags().String("output", "", "Output file for the DataNode")
|
||||
c.Flags().String("format", "datanode", "Output format (datanode, tim, trix, or stim)")
|
||||
c.Flags().String("compression", "none", "Compression format (none, gz, or xz)")
|
||||
c.Flags().String("password", "", "Password for encryption (required for stim/trix format)")
|
||||
c.Flags().StringSlice("exclude", nil, "Patterns to exclude (can be specified multiple times)")
|
||||
c.Flags().Bool("hidden", false, "Include hidden files and directories")
|
||||
c.Flags().Bool("gitignore", true, "Respect .gitignore files (default: true)")
|
||||
return c
|
||||
}
|
||||
|
||||
func init() {
|
||||
collectCmd.AddCommand(&NewCollectLocalCmd().Command)
|
||||
}
|
||||
|
||||
// CollectLocal collects files from a local directory into a DataNode
|
||||
func CollectLocal(directory string, outputFile string, format string, compression string, password string, excludes []string, includeHidden bool, respectGitignore bool) (string, error) {
|
||||
// Validate format
|
||||
if format != "datanode" && format != "tim" && format != "trix" && format != "stim" {
|
||||
return "", fmt.Errorf("invalid format: %s (must be 'datanode', 'tim', 'trix', or 'stim')", format)
|
||||
}
|
||||
if (format == "stim" || format == "trix") && password == "" {
|
||||
return "", fmt.Errorf("password is required for %s format", format)
|
||||
}
|
||||
if compression != "none" && compression != "gz" && compression != "xz" {
|
||||
return "", fmt.Errorf("invalid compression: %s (must be 'none', 'gz', or 'xz')", compression)
|
||||
}
|
||||
|
||||
// Resolve directory path
|
||||
absDir, err := filepath.Abs(directory)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error resolving directory path: %w", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(absDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error accessing directory: %w", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return "", fmt.Errorf("not a directory: %s", absDir)
|
||||
}
|
||||
|
||||
// Load gitignore patterns if enabled
|
||||
var gitignorePatterns []string
|
||||
if respectGitignore {
|
||||
gitignorePatterns = loadGitignore(absDir)
|
||||
}
|
||||
|
||||
// Create DataNode and collect files
|
||||
dn := datanode.New()
|
||||
var fileCount int
|
||||
|
||||
bar := ui.NewProgressBar(-1, "Scanning files")
|
||||
defer bar.Finish()
|
||||
|
||||
err = filepath.WalkDir(absDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get relative path
|
||||
relPath, err := filepath.Rel(absDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip root
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip hidden files/dirs unless explicitly included
|
||||
if !includeHidden && isHidden(relPath) {
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check gitignore patterns
|
||||
if respectGitignore && matchesGitignore(relPath, d.IsDir(), gitignorePatterns) {
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check exclude patterns
|
||||
if matchesExclude(relPath, excludes) {
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip directories (they're implicit in DataNode)
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read file content
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading %s: %w", relPath, err)
|
||||
}
|
||||
|
||||
// Add to DataNode with forward slashes (tar convention)
|
||||
dn.AddData(filepath.ToSlash(relPath), content)
|
||||
fileCount++
|
||||
bar.Describe(fmt.Sprintf("Collected %d files", fileCount))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error walking directory: %w", err)
|
||||
}
|
||||
|
||||
if fileCount == 0 {
|
||||
return "", fmt.Errorf("no files found in %s", directory)
|
||||
}
|
||||
|
||||
bar.Describe(fmt.Sprintf("Packaging %d files", fileCount))
|
||||
|
||||
// Convert to output format
|
||||
var data []byte
|
||||
if format == "tim" {
|
||||
t, err := tim.FromDataNode(dn)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating tim: %w", err)
|
||||
}
|
||||
data, err = t.ToTar()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error serializing tim: %w", err)
|
||||
}
|
||||
} else if format == "stim" {
|
||||
t, err := tim.FromDataNode(dn)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating tim: %w", err)
|
||||
}
|
||||
data, err = t.ToSigil(password)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error encrypting stim: %w", err)
|
||||
}
|
||||
} else if format == "trix" {
|
||||
data, err = trix.ToTrix(dn, password)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error serializing trix: %w", err)
|
||||
}
|
||||
} else {
|
||||
data, err = dn.ToTar()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error serializing DataNode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply compression
|
||||
compressedData, err := compress.Compress(data, compression)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error compressing data: %w", err)
|
||||
}
|
||||
|
||||
// Determine output filename
|
||||
if outputFile == "" {
|
||||
baseName := filepath.Base(absDir)
|
||||
if baseName == "." || baseName == "/" {
|
||||
baseName = "local"
|
||||
}
|
||||
outputFile = baseName + "." + format
|
||||
if compression != "none" {
|
||||
outputFile += "." + compression
|
||||
}
|
||||
}
|
||||
|
||||
err = os.WriteFile(outputFile, compressedData, 0644)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error writing output file: %w", err)
|
||||
}
|
||||
|
||||
return outputFile, nil
|
||||
}
|
||||
|
||||
// isHidden checks if a path component starts with a dot
|
||||
func isHidden(path string) bool {
|
||||
parts := strings.Split(filepath.ToSlash(path), "/")
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, ".") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// loadGitignore loads patterns from .gitignore if it exists
|
||||
func loadGitignore(dir string) []string {
|
||||
var patterns []string
|
||||
|
||||
gitignorePath := filepath.Join(dir, ".gitignore")
|
||||
content, err := os.ReadFile(gitignorePath)
|
||||
if err != nil {
|
||||
return patterns
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
// Skip empty lines and comments
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
patterns = append(patterns, line)
|
||||
}
|
||||
|
||||
return patterns
|
||||
}
|
||||
|
||||
// matchesGitignore checks if a path matches any gitignore pattern
|
||||
func matchesGitignore(path string, isDir bool, patterns []string) bool {
|
||||
for _, pattern := range patterns {
|
||||
// Handle directory-only patterns
|
||||
if strings.HasSuffix(pattern, "/") {
|
||||
if !isDir {
|
||||
continue
|
||||
}
|
||||
pattern = strings.TrimSuffix(pattern, "/")
|
||||
}
|
||||
|
||||
// Handle negation (simplified - just skip negated patterns)
|
||||
if strings.HasPrefix(pattern, "!") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Match against path components
|
||||
matched, _ := filepath.Match(pattern, filepath.Base(path))
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
|
||||
// Also try matching the full path
|
||||
matched, _ = filepath.Match(pattern, path)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle ** patterns (simplified)
|
||||
if strings.Contains(pattern, "**") {
|
||||
simplePattern := strings.ReplaceAll(pattern, "**", "*")
|
||||
matched, _ = filepath.Match(simplePattern, path)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchesExclude checks if a path matches any exclude pattern
|
||||
func matchesExclude(path string, excludes []string) bool {
|
||||
for _, pattern := range excludes {
|
||||
// Match against basename
|
||||
matched, _ := filepath.Match(pattern, filepath.Base(path))
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
|
||||
// Match against full path
|
||||
matched, _ = filepath.Match(pattern, path)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
141
cmd/collect_pwa.go
Normal file
141
cmd/collect_pwa.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
"github.com/Snider/Borg/pkg/pwa"
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
"github.com/Snider/Borg/pkg/ui"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CollectPWACmd struct {
|
||||
cobra.Command
|
||||
PWAClient pwa.PWAClient
|
||||
}
|
||||
|
||||
// NewCollectPWACmd creates a new collect pwa command
|
||||
func NewCollectPWACmd() *CollectPWACmd {
|
||||
c := &CollectPWACmd{
|
||||
PWAClient: pwa.NewPWAClient(),
|
||||
}
|
||||
c.Command = cobra.Command{
|
||||
Use: "pwa [url]",
|
||||
Short: "Collect a single PWA using a URI",
|
||||
Long: `Collect a single PWA and store it in a DataNode.
|
||||
|
||||
Examples:
|
||||
borg collect pwa https://example.com
|
||||
borg collect pwa https://example.com --output mypwa.dat
|
||||
borg collect pwa https://example.com --format stim --password secret`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
pwaURL, _ := cmd.Flags().GetString("uri")
|
||||
// Allow URL as positional argument
|
||||
if len(args) > 0 && pwaURL == "" {
|
||||
pwaURL = args[0]
|
||||
}
|
||||
outputFile, _ := cmd.Flags().GetString("output")
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
compression, _ := cmd.Flags().GetString("compression")
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
|
||||
finalPath, err := CollectPWA(c.PWAClient, pwaURL, outputFile, format, compression, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "PWA saved to", finalPath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
c.Flags().String("uri", "", "The URI of the PWA to collect (can also be passed as positional arg)")
|
||||
c.Flags().String("output", "", "Output file for the DataNode")
|
||||
c.Flags().String("format", "datanode", "Output format (datanode, tim, trix, or stim)")
|
||||
c.Flags().String("compression", "none", "Compression format (none, gz, or xz)")
|
||||
c.Flags().String("password", "", "Password for encryption (required for stim format)")
|
||||
return c
|
||||
}
|
||||
|
||||
func init() {
|
||||
collectCmd.AddCommand(&NewCollectPWACmd().Command)
|
||||
}
|
||||
func CollectPWA(client pwa.PWAClient, pwaURL string, outputFile string, format string, compression string, password string) (string, error) {
|
||||
if pwaURL == "" {
|
||||
return "", fmt.Errorf("url is required")
|
||||
}
|
||||
if format != "datanode" && format != "tim" && format != "trix" && format != "stim" {
|
||||
return "", fmt.Errorf("invalid format: %s (must be 'datanode', 'tim', 'trix', or 'stim')", format)
|
||||
}
|
||||
if format == "stim" && password == "" {
|
||||
return "", fmt.Errorf("password is required for stim format")
|
||||
}
|
||||
if compression != "none" && compression != "gz" && compression != "xz" {
|
||||
return "", fmt.Errorf("invalid compression: %s (must be 'none', 'gz', or 'xz')", compression)
|
||||
}
|
||||
|
||||
bar := ui.NewProgressBar(-1, "Finding PWA manifest")
|
||||
defer bar.Finish()
|
||||
|
||||
manifestURL, err := client.FindManifest(pwaURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error finding manifest: %w", err)
|
||||
}
|
||||
bar.Describe("Downloading and packaging PWA")
|
||||
dn, err := client.DownloadAndPackagePWA(pwaURL, manifestURL, bar)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error downloading and packaging PWA: %w", err)
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if format == "tim" {
|
||||
t, err := tim.FromDataNode(dn)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating tim: %w", err)
|
||||
}
|
||||
data, err = t.ToTar()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error serializing tim: %w", err)
|
||||
}
|
||||
} else if format == "stim" {
|
||||
t, err := tim.FromDataNode(dn)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating tim: %w", err)
|
||||
}
|
||||
data, err = t.ToSigil(password)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error encrypting stim: %w", err)
|
||||
}
|
||||
} else if format == "trix" {
|
||||
data, err = trix.ToTrix(dn, password)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error serializing trix: %w", err)
|
||||
}
|
||||
} else {
|
||||
data, err = dn.ToTar()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error serializing DataNode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
compressedData, err := compress.Compress(data, compression)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error compressing data: %w", err)
|
||||
}
|
||||
|
||||
if outputFile == "" {
|
||||
outputFile = "pwa." + format
|
||||
if compression != "none" {
|
||||
outputFile += "." + compression
|
||||
}
|
||||
}
|
||||
|
||||
err = os.WriteFile(outputFile, compressedData, 0644)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error writing PWA to file: %w", err)
|
||||
}
|
||||
return outputFile, nil
|
||||
}
|
||||
108
cmd/collect_website.go
Normal file
108
cmd/collect_website.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
"github.com/Snider/Borg/pkg/ui"
|
||||
"github.com/Snider/Borg/pkg/website"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// collectWebsiteCmd represents the collect website command
|
||||
var collectWebsiteCmd = NewCollectWebsiteCmd()
|
||||
|
||||
func init() {
|
||||
GetCollectCmd().AddCommand(GetCollectWebsiteCmd())
|
||||
}
|
||||
|
||||
func GetCollectWebsiteCmd() *cobra.Command {
|
||||
return collectWebsiteCmd
|
||||
}
|
||||
|
||||
func NewCollectWebsiteCmd() *cobra.Command {
|
||||
collectWebsiteCmd := &cobra.Command{
|
||||
Use: "website [url]",
|
||||
Short: "Collect a single website",
|
||||
Long: `Collect a single website and store it in a DataNode.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
websiteURL := args[0]
|
||||
outputFile, _ := cmd.Flags().GetString("output")
|
||||
depth, _ := cmd.Flags().GetInt("depth")
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
compression, _ := cmd.Flags().GetString("compression")
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
|
||||
if format != "datanode" && format != "tim" && format != "trix" {
|
||||
return fmt.Errorf("invalid format: %s (must be 'datanode', 'tim', or 'trix')", format)
|
||||
}
|
||||
|
||||
prompter := ui.NewNonInteractivePrompter(ui.GetWebsiteQuote)
|
||||
prompter.Start()
|
||||
defer prompter.Stop()
|
||||
var bar *progressbar.ProgressBar
|
||||
if prompter.IsInteractive() {
|
||||
bar = ui.NewProgressBar(-1, "Crawling website")
|
||||
}
|
||||
|
||||
dn, err := website.DownloadAndPackageWebsite(websiteURL, depth, bar)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error downloading and packaging website: %w", err)
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if format == "tim" {
|
||||
tim, err := tim.FromDataNode(dn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating tim: %w", err)
|
||||
}
|
||||
data, err = tim.ToTar()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing tim: %w", err)
|
||||
}
|
||||
} else if format == "trix" {
|
||||
data, err = trix.ToTrix(dn, password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing trix: %w", err)
|
||||
}
|
||||
} else {
|
||||
data, err = dn.ToTar()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing DataNode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
compressedData, err := compress.Compress(data, compression)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error compressing data: %w", err)
|
||||
}
|
||||
|
||||
if outputFile == "" {
|
||||
outputFile = "website." + format
|
||||
if compression != "none" {
|
||||
outputFile += "." + compression
|
||||
}
|
||||
}
|
||||
|
||||
err = os.WriteFile(outputFile, compressedData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing website to file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "Website saved to", outputFile)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
collectWebsiteCmd.PersistentFlags().String("output", "", "Output file for the DataNode")
|
||||
collectWebsiteCmd.PersistentFlags().Int("depth", 2, "Recursion depth for downloading")
|
||||
collectWebsiteCmd.PersistentFlags().String("format", "datanode", "Output format (datanode, tim, or trix)")
|
||||
collectWebsiteCmd.PersistentFlags().String("compression", "none", "Compression format (none, gz, or xz)")
|
||||
collectWebsiteCmd.PersistentFlags().String("password", "", "Password for encryption")
|
||||
return collectWebsiteCmd
|
||||
}
|
||||
68
cmd/collect_website_test.go
Normal file
68
cmd/collect_website_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/Snider/Borg/pkg/website"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
)
|
||||
|
||||
func TestCollectWebsiteCmd_Good(t *testing.T) {
|
||||
// Mock the website downloader
|
||||
oldDownloadAndPackageWebsite := website.DownloadAndPackageWebsite
|
||||
website.DownloadAndPackageWebsite = func(startURL string, maxDepth int, bar *progressbar.ProgressBar) (*datanode.DataNode, error) {
|
||||
return datanode.New(), nil
|
||||
}
|
||||
defer func() {
|
||||
website.DownloadAndPackageWebsite = oldDownloadAndPackageWebsite
|
||||
}()
|
||||
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetCollectCmd())
|
||||
|
||||
// Execute command
|
||||
out := filepath.Join(t.TempDir(), "out")
|
||||
_, err := executeCommand(rootCmd, "collect", "website", "https://example.com", "--output", out)
|
||||
if err != nil {
|
||||
t.Fatalf("collect website command failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectWebsiteCmd_Bad(t *testing.T) {
|
||||
// Mock the website downloader to return an error
|
||||
oldDownloadAndPackageWebsite := website.DownloadAndPackageWebsite
|
||||
website.DownloadAndPackageWebsite = func(startURL string, maxDepth int, bar *progressbar.ProgressBar) (*datanode.DataNode, error) {
|
||||
return nil, fmt.Errorf("website error")
|
||||
}
|
||||
defer func() {
|
||||
website.DownloadAndPackageWebsite = oldDownloadAndPackageWebsite
|
||||
}()
|
||||
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetCollectCmd())
|
||||
|
||||
// Execute command
|
||||
out := filepath.Join(t.TempDir(), "out")
|
||||
_, err := executeCommand(rootCmd, "collect", "website", "https://example.com", "--output", out)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectWebsiteCmd_Ugly(t *testing.T) {
|
||||
t.Run("No arguments", func(t *testing.T) {
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetCollectCmd())
|
||||
_, err := executeCommand(rootCmd, "collect", "website")
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for no arguments, but got none")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "accepts 1 arg(s), received 0") {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
92
cmd/compile.go
Normal file
92
cmd/compile.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var borgfile string
|
||||
var output string
|
||||
var encryptPassword string
|
||||
|
||||
var compileCmd = NewCompileCmd()
|
||||
|
||||
func NewCompileCmd() *cobra.Command {
|
||||
compileCmd := &cobra.Command{
|
||||
Use: "compile",
|
||||
Short: "Compile a Borgfile into a Terminal Isolation Matrix.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
content, err := os.ReadFile(borgfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m, err := tim.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) == 0 {
|
||||
continue
|
||||
}
|
||||
switch parts[0] {
|
||||
case "ADD":
|
||||
if len(parts) != 3 {
|
||||
return fmt.Errorf("invalid ADD instruction: %s", line)
|
||||
}
|
||||
src := parts[1]
|
||||
dest := parts[2]
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.RootFS.AddData(strings.TrimPrefix(dest, "/"), data)
|
||||
default:
|
||||
return fmt.Errorf("unknown instruction: %s", parts[0])
|
||||
}
|
||||
}
|
||||
|
||||
// If encryption is requested, output as .stim
|
||||
if encryptPassword != "" {
|
||||
stimData, err := m.ToSigil(encryptPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputPath := output
|
||||
if !strings.HasSuffix(outputPath, ".stim") {
|
||||
outputPath = strings.TrimSuffix(outputPath, ".tim") + ".stim"
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Compiled encrypted TIM to %s\n", outputPath)
|
||||
return os.WriteFile(outputPath, stimData, 0644)
|
||||
}
|
||||
|
||||
// Original unencrypted output
|
||||
tarball, err := m.ToTar()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Compiled TIM to %s\n", output)
|
||||
return os.WriteFile(output, tarball, 0644)
|
||||
},
|
||||
}
|
||||
compileCmd.Flags().StringVarP(&borgfile, "file", "f", "Borgfile", "Path to the Borgfile.")
|
||||
compileCmd.Flags().StringVarP(&output, "output", "o", "a.tim", "Path to the output tim file.")
|
||||
compileCmd.Flags().StringVarP(&encryptPassword, "encrypt", "e", "", "Encrypt with ChaCha20-Poly1305 using this password (outputs .stim)")
|
||||
return compileCmd
|
||||
}
|
||||
|
||||
func GetCompileCmd() *cobra.Command {
|
||||
return compileCmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(GetCompileCmd())
|
||||
}
|
||||
122
cmd/compile_test.go
Normal file
122
cmd/compile_test.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompileCmd(t *testing.T) {
|
||||
// t.Run("Good", func(t *testing.T) {
|
||||
// tempDir := t.TempDir()
|
||||
// outputTimPath := filepath.Join(tempDir, "test.tim")
|
||||
// borgfilePath := filepath.Join(tempDir, "Borgfile")
|
||||
// dummyFilePath := filepath.Join(tempDir, "dummy.txt")
|
||||
|
||||
// // Create a dummy file to add to the tim.
|
||||
// err := os.WriteFile(dummyFilePath, []byte("dummy content"), 0644)
|
||||
// if err != nil {
|
||||
// t.Fatalf("failed to create dummy file: %v", err)
|
||||
// }
|
||||
|
||||
// // Create a Borgfile.
|
||||
// borgfileContent := "ADD " + dummyFilePath + " /dummy.txt"
|
||||
// err = os.WriteFile(borgfilePath, []byte(borgfileContent), 0644)
|
||||
// if err != nil {
|
||||
// t.Fatalf("failed to create Borgfile: %v", err)
|
||||
// }
|
||||
|
||||
// // Execute the compile command.
|
||||
// cmd := NewCompileCmd()
|
||||
// cmd.SetArgs([]string{"--file", borgfilePath, "--output", outputTimPath})
|
||||
// err = cmd.Execute()
|
||||
// if err != nil {
|
||||
// t.Fatalf("compile command failed: %v", err)
|
||||
// }
|
||||
|
||||
// // Verify the output tim file.
|
||||
// timFile, err := os.Open(outputTimPath)
|
||||
// if err != nil {
|
||||
// t.Fatalf("failed to open output tim file: %v", err)
|
||||
// }
|
||||
// defer timFile.Close()
|
||||
|
||||
// tr := tar.NewReader(timFile)
|
||||
// files := []string{"config.json", "rootfs/", "rootfs/dummy.txt"}
|
||||
// found := make(map[string]bool)
|
||||
// for {
|
||||
// hdr, err := tr.Next()
|
||||
// if err != nil {
|
||||
// break
|
||||
// }
|
||||
// found[hdr.Name] = true
|
||||
// }
|
||||
// for _, f := range files {
|
||||
// if !found[f] {
|
||||
// t.Errorf("%s not found in tim tarball", f)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
t.Run("Bad_Borgfile", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
outputTimPath := filepath.Join(tempDir, "test.tim")
|
||||
borgfilePath := filepath.Join(tempDir, "Borgfile")
|
||||
|
||||
// Create a Borgfile with an invalid instruction.
|
||||
borgfileContent := "INVALID instruction"
|
||||
err := os.WriteFile(borgfilePath, []byte(borgfileContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create Borgfile: %v", err)
|
||||
}
|
||||
|
||||
// Execute the compile command.
|
||||
cmd := NewCompileCmd()
|
||||
cmd.SetArgs([]string{"--file", borgfilePath, "--output", outputTimPath})
|
||||
err = cmd.Execute()
|
||||
if err == nil {
|
||||
t.Error("compile command should have failed but did not")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Bad_ADD", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
outputTimPath := filepath.Join(tempDir, "test.tim")
|
||||
borgfilePath := filepath.Join(tempDir, "Borgfile")
|
||||
|
||||
// Create a Borgfile with an invalid ADD instruction.
|
||||
borgfileContent := "ADD dummy.txt"
|
||||
err := os.WriteFile(borgfilePath, []byte(borgfileContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create Borgfile: %v", err)
|
||||
}
|
||||
|
||||
// Execute the compile command.
|
||||
cmd := NewCompileCmd()
|
||||
cmd.SetArgs([]string{"--file", borgfilePath, "--output", outputTimPath})
|
||||
err = cmd.Execute()
|
||||
if err == nil {
|
||||
t.Error("compile command should have failed but did not")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Ugly_EmptyBorgfile", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
outputTimPath := filepath.Join(tempDir, "test.tim")
|
||||
borgfilePath := filepath.Join(tempDir, "Borgfile")
|
||||
|
||||
// Create an empty Borgfile.
|
||||
err := os.WriteFile(borgfilePath, []byte{}, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create Borgfile: %v", err)
|
||||
}
|
||||
|
||||
// Execute the compile command.
|
||||
cmd := NewCompileCmd()
|
||||
cmd.SetArgs([]string{"--file", borgfilePath, "--output", outputTimPath})
|
||||
err = cmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("compile command failed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
163
cmd/console.go
Normal file
163
cmd/console.go
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Snider/Borg/pkg/console"
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var consoleCmd = NewConsoleCmd()
|
||||
|
||||
// NewConsoleCmd creates the console parent command.
|
||||
func NewConsoleCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "console",
|
||||
Short: "Manage encrypted PWA console demos",
|
||||
Long: `The Borg Console packages and serves encrypted PWA demos.
|
||||
|
||||
Build a console STIM:
|
||||
borg console build -p "password" -o console.stim
|
||||
|
||||
Serve with unlock page:
|
||||
borg console serve console.stim --open
|
||||
|
||||
Serve pre-unlocked:
|
||||
borg console serve console.stim -p "password" --open`,
|
||||
}
|
||||
|
||||
cmd.AddCommand(NewConsoleBuildCmd())
|
||||
cmd.AddCommand(NewConsoleServeCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewConsoleBuildCmd creates the build subcommand.
|
||||
func NewConsoleBuildCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "build",
|
||||
Short: "Build a console STIM from demo files",
|
||||
Long: `Packages HTML demo files into an encrypted STIM container.
|
||||
|
||||
By default, looks for files in js/borg-stmf/ directory.
|
||||
Required files: index.html, support-reply.html, stmf.wasm, wasm_exec.js`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
sourceDir, _ := cmd.Flags().GetString("source")
|
||||
|
||||
if password == "" {
|
||||
return fmt.Errorf("password is required")
|
||||
}
|
||||
|
||||
// Create new TIM
|
||||
m, err := tim.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating TIM: %w", err)
|
||||
}
|
||||
|
||||
// Required demo files
|
||||
files := []string{
|
||||
"index.html",
|
||||
"support-reply.html",
|
||||
"stmf.wasm",
|
||||
"wasm_exec.js",
|
||||
}
|
||||
|
||||
// Add each file to the TIM
|
||||
for _, f := range files {
|
||||
path := filepath.Join(sourceDir, f)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %s: %w", f, err)
|
||||
}
|
||||
m.RootFS.AddData(f, data)
|
||||
fmt.Printf(" + %s (%d bytes)\n", f, len(data))
|
||||
}
|
||||
|
||||
// Encrypt to STIM
|
||||
stim, err := m.ToSigil(password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypting STIM: %w", err)
|
||||
}
|
||||
|
||||
// Write output
|
||||
if err := os.WriteFile(output, stim, 0644); err != nil {
|
||||
return fmt.Errorf("writing output: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\nBuilt: %s (%d bytes)\n", output, len(stim))
|
||||
fmt.Println("Encrypted with ChaCha20-Poly1305")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringP("password", "p", "", "Encryption password (required)")
|
||||
cmd.Flags().StringP("output", "o", "console.stim", "Output file")
|
||||
cmd.Flags().StringP("source", "s", "js/borg-stmf", "Source directory")
|
||||
cmd.MarkFlagRequired("password")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewConsoleServeCmd creates the serve subcommand.
|
||||
func NewConsoleServeCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "serve [stim-file]",
|
||||
Short: "Serve an encrypted console STIM",
|
||||
Long: `Starts an HTTP server to serve encrypted STIM content.
|
||||
|
||||
Without a password, shows a dark-themed unlock page.
|
||||
With a password, decrypts immediately and serves content.
|
||||
|
||||
Examples:
|
||||
borg console serve demos.stim --open
|
||||
borg console serve demos.stim -p "password" --port 3000`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
stimPath := args[0]
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
port, _ := cmd.Flags().GetString("port")
|
||||
openBrowser, _ := cmd.Flags().GetBool("open")
|
||||
|
||||
// Create server
|
||||
server, err := console.NewServer(stimPath, password, port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Print status
|
||||
fmt.Printf("Borg Console serving at %s\n", server.URL())
|
||||
if password != "" {
|
||||
fmt.Println("Status: Unlocked (password provided)")
|
||||
} else {
|
||||
fmt.Println("Status: Locked (unlock page active)")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Open browser if requested
|
||||
if openBrowser {
|
||||
if err := console.OpenBrowser(server.URL()); err != nil {
|
||||
fmt.Printf("Warning: could not open browser: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start serving
|
||||
return server.Start()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringP("password", "p", "", "Decryption password (skip unlock page)")
|
||||
cmd.Flags().String("port", "8080", "Port to serve on")
|
||||
cmd.Flags().Bool("open", false, "Auto-open browser")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(consoleCmd)
|
||||
}
|
||||
3
cmd/dapp-fm-app/.gitignore
vendored
Normal file
3
cmd/dapp-fm-app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
build/
|
||||
*.exe
|
||||
dapp-fm-app
|
||||
987
cmd/dapp-fm-app/frontend/index.html
Normal file
987
cmd/dapp-fm-app/frontend/index.html
Normal file
|
|
@ -0,0 +1,987 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>dapp.fm - Decentralized Music Player</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #0f0f1a 0%, #1a0a2e 50%, #0f1a2e 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #ff006e 0%, #8338ec 50%, #3a86ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.logo .tagline {
|
||||
color: #888;
|
||||
font-size: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.hero-text {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.hero-text p {
|
||||
color: #aaa;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hero-text strong {
|
||||
color: #ff006e;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card h2 .icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
textarea, input[type="password"], input[type="text"], input[type="url"] {
|
||||
width: 100%;
|
||||
padding: 1rem 1.25rem;
|
||||
border: 2px solid rgba(255,255,255,0.1);
|
||||
border-radius: 12px;
|
||||
background: rgba(0,0,0,0.4);
|
||||
color: #fff;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
textarea:focus, input:focus {
|
||||
outline: none;
|
||||
border-color: #8338ec;
|
||||
box-shadow: 0 0 0 4px rgba(131, 56, 236, 0.2);
|
||||
}
|
||||
|
||||
textarea.encrypted {
|
||||
min-height: 100px;
|
||||
font-size: 0.75rem;
|
||||
word-break: break-all;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.unlock-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.unlock-row .input-group {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 1rem 2.5rem;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: linear-gradient(135deg, #ff006e 0%, #8338ec 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 20px rgba(255, 0, 110, 0.3);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 30px rgba(255, 0, 110, 0.4);
|
||||
}
|
||||
|
||||
button.primary:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.status-indicator .dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-indicator.loading .dot {
|
||||
background: #ffc107;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.ready .dot {
|
||||
background: #00ff94;
|
||||
}
|
||||
|
||||
.status-indicator.error .dot {
|
||||
background: #ff5252;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
background: rgba(255, 82, 82, 0.15);
|
||||
border: 1px solid rgba(255, 82, 82, 0.4);
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
display: none;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.error-banner.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Media Player Styles */
|
||||
.player-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.player-container.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.track-info {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.track-artwork {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto 1.5rem;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #2d1b4e 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 5rem;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.track-artwork img, .track-artwork video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.track-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.track-artist {
|
||||
color: #888;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.media-player-wrapper {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
audio, video {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
video {
|
||||
max-height: 500px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.video-player-wrapper {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.license-info {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(131, 56, 236, 0.1);
|
||||
border: 1px solid rgba(131, 56, 236, 0.3);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.license-info h4 {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: #8338ec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.license-info p {
|
||||
font-size: 0.85rem;
|
||||
color: #aaa;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.license-info .license-token {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.75rem;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
margin-top: 0.75rem;
|
||||
word-break: break-all;
|
||||
color: #00ff94;
|
||||
}
|
||||
|
||||
.download-section {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.download-section button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
.track-list-section {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.track-list-section h3 {
|
||||
font-size: 0.9rem;
|
||||
color: #888;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.track-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.track-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.track-item:hover {
|
||||
background: rgba(131, 56, 236, 0.2);
|
||||
}
|
||||
|
||||
.track-item.active {
|
||||
background: rgba(255, 0, 110, 0.2);
|
||||
border: 1px solid rgba(255, 0, 110, 0.4);
|
||||
}
|
||||
|
||||
.track-number {
|
||||
font-weight: 700;
|
||||
color: #8338ec;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.track-name {
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.track-type {
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.track-time {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #00ff94;
|
||||
}
|
||||
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.file-input-wrapper input[type="file"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-input-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
padding: 2rem;
|
||||
border: 2px dashed rgba(255,255,255,0.2);
|
||||
border-radius: 12px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-input-label:hover {
|
||||
border-color: #8338ec;
|
||||
background: rgba(131, 56, 236, 0.1);
|
||||
}
|
||||
|
||||
.file-input-label .icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.or-divider {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin: 1rem 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.native-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: linear-gradient(135deg, #00ff94 0%, #00d4aa 100%);
|
||||
color: #000;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<h1>dapp.fm</h1>
|
||||
<p class="tagline">Decentralized Music Distribution <span class="native-badge">Native App</span></p>
|
||||
</div>
|
||||
|
||||
<div class="hero-text">
|
||||
<p>
|
||||
<strong>No middlemen. No platforms. No 70% cuts.</strong><br>
|
||||
Artists encrypt their music with ChaCha20-Poly1305. Fans unlock with a license token.
|
||||
Content lives on any CDN, IPFS, or artist's own server. The password IS the license.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status-indicator ready">
|
||||
<span class="dot"></span>
|
||||
<span>Native decryption ready (memory speed)</span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><span class="icon">🔐</span> Unlock Licensed Content</h2>
|
||||
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" id="file-input" accept=".smsg,.enc,.borg">
|
||||
<label class="file-input-label">
|
||||
<span class="icon">📁</span>
|
||||
<span>Drop encrypted file here or click to browse</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="or-divider">- or paste encrypted content -</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="encrypted-content">Encrypted Content (base64):</label>
|
||||
<textarea id="encrypted-content" class="encrypted" placeholder="Paste the encrypted content from the artist..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="demo-banner" style="background: rgba(255, 0, 110, 0.1); border: 1px solid rgba(255, 0, 110, 0.3); border-radius: 12px; padding: 1rem; margin-bottom: 1rem;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem;">
|
||||
<div>
|
||||
<strong style="color: #ff006e;">Try the Demo!</strong>
|
||||
<span style="color: #888; font-size: 0.85rem; margin-left: 0.5rem;">Bundled sample video</span>
|
||||
</div>
|
||||
<button id="load-demo-btn" class="secondary" style="padding: 0.6rem 1.2rem; font-size: 0.85rem;">Load Demo Track</button>
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: #666; margin-top: 0.5rem;">
|
||||
Password: <code style="background: rgba(0,0,0,0.3); padding: 0.2rem 0.5rem; border-radius: 4px; color: #00ff94;">PMVXogAJNVe_DDABfTmLYztaJAzsD0R7</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error-banner" class="error-banner"></div>
|
||||
|
||||
<!-- Manifest preview (shown without decryption) -->
|
||||
<div id="manifest-preview" style="display: none; background: rgba(131, 56, 236, 0.1); border: 1px solid rgba(131, 56, 236, 0.3); border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem;"></div>
|
||||
|
||||
<div class="unlock-row">
|
||||
<div class="input-group">
|
||||
<label for="license-token">License Token (Password):</label>
|
||||
<input type="password" id="license-token" placeholder="Enter your license token from the artist">
|
||||
</div>
|
||||
<button id="unlock-btn" class="primary">Unlock</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player appears after unlock -->
|
||||
<div id="player-container" class="card player-container">
|
||||
<h2><span class="icon">🎵</span> Now Playing</h2>
|
||||
|
||||
<div class="track-info">
|
||||
<div class="track-artwork" id="track-artwork">🎶</div>
|
||||
<div class="track-title" id="track-title">Track Title</div>
|
||||
<div class="track-artist" id="track-artist">Artist Name</div>
|
||||
</div>
|
||||
|
||||
<div class="media-player-wrapper" id="media-player-wrapper">
|
||||
<!-- Audio/Video player inserted here -->
|
||||
</div>
|
||||
|
||||
<div id="track-list-section" class="track-list-section" style="display: none;">
|
||||
<h3><span>💿</span> Track List</h3>
|
||||
<div id="track-list" class="track-list">
|
||||
<!-- Tracks populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="license-info">
|
||||
<h4>🔓 Licensed Content</h4>
|
||||
<p id="license-description">This content was unlocked with your personal license token.
|
||||
Decryption powered by native Go - no servers, memory speed.</p>
|
||||
<div class="license-token" id="license-display"></div>
|
||||
</div>
|
||||
|
||||
<div class="download-section">
|
||||
<button class="secondary" id="download-btn">Download Original</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Wails runtime - provides window.go bindings
|
||||
let currentMediaBlob = null;
|
||||
let currentMediaName = null;
|
||||
let currentMediaMime = null;
|
||||
let currentManifest = null;
|
||||
|
||||
// Check if Wails runtime is available
|
||||
function isWailsReady() {
|
||||
return typeof window.go !== 'undefined' &&
|
||||
typeof window.go.player !== 'undefined' &&
|
||||
typeof window.go.player.Player !== 'undefined';
|
||||
}
|
||||
|
||||
// Wait for Wails runtime
|
||||
function waitForWails() {
|
||||
return new Promise((resolve) => {
|
||||
if (isWailsReady()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
// Poll for Wails runtime
|
||||
const interval = setInterval(() => {
|
||||
if (isWailsReady()) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const errorBanner = document.getElementById('error-banner');
|
||||
errorBanner.textContent = msg;
|
||||
errorBanner.classList.add('visible');
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
document.getElementById('error-banner').classList.remove('visible');
|
||||
}
|
||||
|
||||
// Handle file input
|
||||
document.getElementById('file-input').addEventListener('change', async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const content = await file.arrayBuffer();
|
||||
const base64 = btoa(String.fromCharCode(...new Uint8Array(content)));
|
||||
document.getElementById('encrypted-content').value = base64;
|
||||
await showManifestPreview(base64);
|
||||
} catch (err) {
|
||||
showError('Failed to read file: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for content paste/input
|
||||
let previewDebounce = null;
|
||||
document.getElementById('encrypted-content').addEventListener('input', async (e) => {
|
||||
const content = e.target.value.trim();
|
||||
clearTimeout(previewDebounce);
|
||||
previewDebounce = setTimeout(async () => {
|
||||
if (content && content.length > 100) {
|
||||
await showManifestPreview(content);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Show manifest preview using Go bindings (NO WASM!)
|
||||
async function showManifestPreview(encryptedB64) {
|
||||
await waitForWails();
|
||||
|
||||
try {
|
||||
// Direct Go call at memory speed!
|
||||
const manifest = await window.go.player.Player.GetManifest(encryptedB64);
|
||||
currentManifest = manifest;
|
||||
|
||||
const previewSection = document.getElementById('manifest-preview');
|
||||
while (previewSection.firstChild) {
|
||||
previewSection.removeChild(previewSection.firstChild);
|
||||
}
|
||||
|
||||
if (manifest && manifest.title) {
|
||||
previewSection.style.display = 'block';
|
||||
|
||||
// Header with icon
|
||||
const headerDiv = document.createElement('div');
|
||||
headerDiv.style.cssText = 'display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;';
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.style.fontSize = '2.5rem';
|
||||
icon.textContent = manifest.release_type === 'djset' ? '🎧' :
|
||||
manifest.release_type === 'live' ? '🎤' : '💿';
|
||||
|
||||
const titleDiv = document.createElement('div');
|
||||
const titleEl = document.createElement('div');
|
||||
titleEl.style.cssText = 'font-size: 1.2rem; font-weight: 700; color: #fff;';
|
||||
titleEl.textContent = manifest.title || 'Untitled';
|
||||
|
||||
const artistEl = document.createElement('div');
|
||||
artistEl.style.cssText = 'font-size: 0.9rem; color: #888;';
|
||||
artistEl.textContent = manifest.artist || 'Unknown Artist';
|
||||
|
||||
titleDiv.appendChild(titleEl);
|
||||
titleDiv.appendChild(artistEl);
|
||||
headerDiv.appendChild(icon);
|
||||
headerDiv.appendChild(titleDiv);
|
||||
previewSection.appendChild(headerDiv);
|
||||
|
||||
// Track list
|
||||
if (manifest.tracks && manifest.tracks.length > 0) {
|
||||
const trackHeader = document.createElement('div');
|
||||
trackHeader.style.cssText = 'font-size: 0.85rem; color: #8338ec; margin-bottom: 0.5rem;';
|
||||
trackHeader.textContent = '💿 ' + manifest.tracks.length + ' track(s)';
|
||||
previewSection.appendChild(trackHeader);
|
||||
|
||||
const trackList = document.createElement('div');
|
||||
trackList.style.maxHeight = '150px';
|
||||
trackList.style.overflowY = 'auto';
|
||||
|
||||
manifest.tracks.forEach((track, i) => {
|
||||
const trackEl = document.createElement('div');
|
||||
trackEl.style.cssText = 'display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 6px; margin-bottom: 0.25rem; font-size: 0.85rem;';
|
||||
|
||||
const numEl = document.createElement('span');
|
||||
numEl.style.cssText = 'color: #8338ec; font-weight: 600; min-width: 20px;';
|
||||
numEl.textContent = (track.track_num || (i + 1)) + '.';
|
||||
|
||||
const nameEl = document.createElement('span');
|
||||
nameEl.style.cssText = 'flex: 1; color: #ccc;';
|
||||
nameEl.textContent = track.title || 'Track ' + (i + 1);
|
||||
|
||||
const timeEl = document.createElement('span');
|
||||
timeEl.style.cssText = 'color: #00ff94; font-family: monospace; font-size: 0.8rem;';
|
||||
timeEl.textContent = formatTime(track.start || 0);
|
||||
|
||||
trackEl.appendChild(numEl);
|
||||
trackEl.appendChild(nameEl);
|
||||
trackEl.appendChild(timeEl);
|
||||
trackList.appendChild(trackEl);
|
||||
});
|
||||
|
||||
previewSection.appendChild(trackList);
|
||||
}
|
||||
|
||||
// License status
|
||||
if (manifest.is_expired !== undefined) {
|
||||
const licenseDiv = document.createElement('div');
|
||||
licenseDiv.style.cssText = 'margin-top: 1rem; padding: 0.75rem; border-radius: 8px;';
|
||||
|
||||
if (manifest.is_expired) {
|
||||
licenseDiv.style.background = 'rgba(255, 82, 82, 0.2)';
|
||||
licenseDiv.style.border = '1px solid rgba(255, 82, 82, 0.4)';
|
||||
const label = document.createElement('div');
|
||||
label.style.cssText = 'color: #ff5252; font-weight: 600;';
|
||||
label.textContent = 'LICENSE EXPIRED';
|
||||
licenseDiv.appendChild(label);
|
||||
} else if (manifest.time_remaining) {
|
||||
licenseDiv.style.background = 'rgba(0, 255, 148, 0.1)';
|
||||
licenseDiv.style.border = '1px solid rgba(0, 255, 148, 0.3)';
|
||||
const label = document.createElement('span');
|
||||
label.style.cssText = 'color: #00ff94; font-weight: 600; font-size: 0.8rem;';
|
||||
label.textContent = (manifest.license_type || 'LICENSE').toUpperCase();
|
||||
const time = document.createElement('span');
|
||||
time.style.cssText = 'color: #888; font-size: 0.8rem; margin-left: 0.5rem;';
|
||||
time.textContent = manifest.time_remaining + ' remaining';
|
||||
licenseDiv.appendChild(label);
|
||||
licenseDiv.appendChild(time);
|
||||
} else {
|
||||
licenseDiv.style.background = 'rgba(0, 255, 148, 0.1)';
|
||||
licenseDiv.style.border = '1px solid rgba(0, 255, 148, 0.3)';
|
||||
const label = document.createElement('span');
|
||||
label.style.cssText = 'color: #00ff94; font-weight: 600; font-size: 0.8rem;';
|
||||
label.textContent = 'PERPETUAL LICENSE';
|
||||
licenseDiv.appendChild(label);
|
||||
}
|
||||
previewSection.appendChild(licenseDiv);
|
||||
}
|
||||
|
||||
const hint = document.createElement('div');
|
||||
hint.style.cssText = 'margin-top: 1rem; font-size: 0.85rem; color: #888; text-align: center;';
|
||||
hint.textContent = manifest.is_expired ?
|
||||
'License expired. Contact artist for renewal.' :
|
||||
'Enter license token to unlock and play';
|
||||
previewSection.appendChild(hint);
|
||||
|
||||
} else {
|
||||
previewSection.style.display = 'none';
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Could not read manifest:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock content using Go bindings (memory speed!)
|
||||
async function unlockContent() {
|
||||
hideError();
|
||||
await waitForWails();
|
||||
|
||||
const encryptedB64 = document.getElementById('encrypted-content').value.trim();
|
||||
const password = document.getElementById('license-token').value;
|
||||
|
||||
if (!encryptedB64) {
|
||||
showError('Please provide encrypted content');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
showError('Please enter your license token');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check license validity (memory speed)
|
||||
const isValid = await window.go.player.Player.IsLicenseValid(encryptedB64);
|
||||
if (!isValid) {
|
||||
showError('License has expired. Contact the artist for renewal.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrypt using Go bindings (memory speed - no HTTP/TCP!)
|
||||
const result = await window.go.player.Player.Decrypt(encryptedB64, password);
|
||||
displayMedia(result, password);
|
||||
|
||||
} catch (err) {
|
||||
showError('Unlock failed: ' + err.message);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Display decrypted media
|
||||
function displayMedia(result, password) {
|
||||
const playerContainer = document.getElementById('player-container');
|
||||
const mediaWrapper = document.getElementById('media-player-wrapper');
|
||||
const artworkEl = document.getElementById('track-artwork');
|
||||
|
||||
// Set track info
|
||||
const title = (currentManifest && currentManifest.title) || result.subject || 'Untitled';
|
||||
const artist = (currentManifest && currentManifest.artist) || result.from || 'Unknown Artist';
|
||||
document.getElementById('track-title').textContent = title;
|
||||
document.getElementById('track-artist').textContent = artist;
|
||||
|
||||
// Show masked license token
|
||||
const masked = password.substring(0, 4) + '••••••••' + password.substring(password.length - 4);
|
||||
document.getElementById('license-display').textContent = masked;
|
||||
|
||||
// Clear previous media
|
||||
while (mediaWrapper.firstChild) mediaWrapper.removeChild(mediaWrapper.firstChild);
|
||||
while (artworkEl.firstChild) artworkEl.removeChild(artworkEl.firstChild);
|
||||
artworkEl.textContent = '🎶';
|
||||
|
||||
// Process attachments
|
||||
if (result.attachments && result.attachments.length > 0) {
|
||||
result.attachments.forEach((att) => {
|
||||
const mime = att.mime_type || 'application/octet-stream';
|
||||
|
||||
// URL from Go - served through Wails asset handler
|
||||
const url = att.url || att.file_path || att.stream_url || att.data_url;
|
||||
|
||||
// Store info for download
|
||||
currentMediaName = att.name;
|
||||
currentMediaMime = mime;
|
||||
|
||||
if (mime.startsWith('video/')) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'video-player-wrapper';
|
||||
const video = document.createElement('video');
|
||||
video.controls = true;
|
||||
video.src = url;
|
||||
video.style.width = '100%';
|
||||
wrapper.appendChild(video);
|
||||
mediaWrapper.appendChild(wrapper);
|
||||
artworkEl.textContent = '🎬';
|
||||
|
||||
} else if (mime.startsWith('audio/')) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'audio-player';
|
||||
const audio = document.createElement('audio');
|
||||
audio.controls = true;
|
||||
audio.src = url;
|
||||
audio.style.width = '100%';
|
||||
wrapper.appendChild(audio);
|
||||
mediaWrapper.appendChild(wrapper);
|
||||
artworkEl.textContent = '🎵';
|
||||
|
||||
} else if (mime.startsWith('image/')) {
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
artworkEl.textContent = '';
|
||||
artworkEl.appendChild(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Build track list from manifest
|
||||
const trackListSection = document.getElementById('track-list-section');
|
||||
const trackListEl = document.getElementById('track-list');
|
||||
while (trackListEl.firstChild) trackListEl.removeChild(trackListEl.firstChild);
|
||||
|
||||
if (currentManifest && currentManifest.tracks && currentManifest.tracks.length > 0) {
|
||||
trackListSection.style.display = 'block';
|
||||
|
||||
currentManifest.tracks.forEach((track, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'track-item';
|
||||
item.addEventListener('click', () => {
|
||||
const media = document.querySelector('audio, video');
|
||||
if (media) {
|
||||
media.currentTime = track.start || 0;
|
||||
media.play();
|
||||
document.querySelectorAll('.track-item').forEach(t => t.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
const num = document.createElement('span');
|
||||
num.className = 'track-number';
|
||||
num.textContent = track.track_num || (index + 1);
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.style.flex = '1';
|
||||
const name = document.createElement('div');
|
||||
name.className = 'track-name';
|
||||
name.textContent = track.title || 'Track ' + (index + 1);
|
||||
info.appendChild(name);
|
||||
|
||||
const time = document.createElement('span');
|
||||
time.className = 'track-time';
|
||||
time.textContent = formatTime(track.start || 0);
|
||||
|
||||
item.appendChild(num);
|
||||
item.appendChild(info);
|
||||
item.appendChild(time);
|
||||
trackListEl.appendChild(item);
|
||||
});
|
||||
} else {
|
||||
trackListSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update license description
|
||||
if (currentManifest && currentManifest.time_remaining) {
|
||||
document.getElementById('license-description').textContent =
|
||||
(currentManifest.license_type || 'Rental').toUpperCase() + ' license - ' +
|
||||
currentManifest.time_remaining + ' remaining. Native Go decryption at memory speed.';
|
||||
}
|
||||
|
||||
// Hide preview, show player
|
||||
document.getElementById('manifest-preview').style.display = 'none';
|
||||
playerContainer.classList.add('visible');
|
||||
playerContainer.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return mins + ':' + secs.toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
// Download handler
|
||||
document.getElementById('download-btn').addEventListener('click', () => {
|
||||
if (!currentMediaBlob) {
|
||||
alert('No media to download');
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(currentMediaBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = currentMediaName || 'media';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
// Load bundled demo - DIRECT GO CALL, no HTTP!
|
||||
async function loadDemo() {
|
||||
const btn = document.getElementById('load-demo-btn');
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = 'Loading...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
await waitForWails();
|
||||
|
||||
// Get manifest first (direct Go call)
|
||||
const manifest = await window.go.main.App.GetDemoManifest();
|
||||
currentManifest = manifest;
|
||||
|
||||
// Decrypt demo directly in Go - NO fetch, NO base64 encoding!
|
||||
// Go reads embedded bytes -> decrypts -> returns result
|
||||
const result = await window.go.main.App.LoadDemo();
|
||||
|
||||
// Display the decrypted media
|
||||
displayMedia(result, 'PMVXogAJNVe_DDABfTmLYztaJAzsD0R7');
|
||||
|
||||
btn.textContent = 'Loaded!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
btn.disabled = false;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
showError('Failed to load demo: ' + err.message);
|
||||
btn.textContent = originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('unlock-btn').addEventListener('click', unlockContent);
|
||||
document.getElementById('license-token').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') unlockContent();
|
||||
});
|
||||
document.getElementById('load-demo-btn').addEventListener('click', loadDemo);
|
||||
|
||||
// Ready check
|
||||
waitForWails().then(() => {
|
||||
console.log('Wails bindings ready - memory speed decryption enabled');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
14
cmd/dapp-fm-app/frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
14
cmd/dapp-fm-app/frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {main} from '../models';
|
||||
import {player} from '../models';
|
||||
|
||||
export function DecryptAndServe(arg1:string,arg2:string):Promise<main.MediaResult>;
|
||||
|
||||
export function GetDemoManifest():Promise<player.ManifestInfo>;
|
||||
|
||||
export function GetManifest(arg1:string):Promise<player.ManifestInfo>;
|
||||
|
||||
export function IsLicenseValid(arg1:string):Promise<boolean>;
|
||||
|
||||
export function LoadDemo():Promise<main.MediaResult>;
|
||||
23
cmd/dapp-fm-app/frontend/wailsjs/go/main/App.js
Executable file
23
cmd/dapp-fm-app/frontend/wailsjs/go/main/App.js
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function DecryptAndServe(arg1, arg2) {
|
||||
return window['go']['main']['App']['DecryptAndServe'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetDemoManifest() {
|
||||
return window['go']['main']['App']['GetDemoManifest']();
|
||||
}
|
||||
|
||||
export function GetManifest(arg1) {
|
||||
return window['go']['main']['App']['GetManifest'](arg1);
|
||||
}
|
||||
|
||||
export function IsLicenseValid(arg1) {
|
||||
return window['go']['main']['App']['IsLicenseValid'](arg1);
|
||||
}
|
||||
|
||||
export function LoadDemo() {
|
||||
return window['go']['main']['App']['LoadDemo']();
|
||||
}
|
||||
140
cmd/dapp-fm-app/frontend/wailsjs/go/models.ts
Executable file
140
cmd/dapp-fm-app/frontend/wailsjs/go/models.ts
Executable file
|
|
@ -0,0 +1,140 @@
|
|||
export namespace main {
|
||||
|
||||
export class MediaAttachment {
|
||||
name: string;
|
||||
mime_type: string;
|
||||
size: number;
|
||||
url: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new MediaAttachment(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.mime_type = source["mime_type"];
|
||||
this.size = source["size"];
|
||||
this.url = source["url"];
|
||||
}
|
||||
}
|
||||
export class MediaResult {
|
||||
body: string;
|
||||
subject?: string;
|
||||
from?: string;
|
||||
attachments?: MediaAttachment[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new MediaResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.body = source["body"];
|
||||
this.subject = source["subject"];
|
||||
this.from = source["from"];
|
||||
this.attachments = this.convertValues(source["attachments"], MediaAttachment);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace player {
|
||||
|
||||
export class TrackInfo {
|
||||
title: string;
|
||||
start: number;
|
||||
end?: number;
|
||||
type?: string;
|
||||
track_num?: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new TrackInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.title = source["title"];
|
||||
this.start = source["start"];
|
||||
this.end = source["end"];
|
||||
this.type = source["type"];
|
||||
this.track_num = source["track_num"];
|
||||
}
|
||||
}
|
||||
export class ManifestInfo {
|
||||
title?: string;
|
||||
artist?: string;
|
||||
album?: string;
|
||||
genre?: string;
|
||||
year?: number;
|
||||
release_type?: string;
|
||||
duration?: number;
|
||||
format?: string;
|
||||
expires_at?: number;
|
||||
issued_at?: number;
|
||||
license_type?: string;
|
||||
tracks?: TrackInfo[];
|
||||
is_expired: boolean;
|
||||
time_remaining?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ManifestInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.title = source["title"];
|
||||
this.artist = source["artist"];
|
||||
this.album = source["album"];
|
||||
this.genre = source["genre"];
|
||||
this.year = source["year"];
|
||||
this.release_type = source["release_type"];
|
||||
this.duration = source["duration"];
|
||||
this.format = source["format"];
|
||||
this.expires_at = source["expires_at"];
|
||||
this.issued_at = source["issued_at"];
|
||||
this.license_type = source["license_type"];
|
||||
this.tracks = this.convertValues(source["tracks"], TrackInfo);
|
||||
this.is_expired = source["is_expired"];
|
||||
this.time_remaining = source["time_remaining"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
24
cmd/dapp-fm-app/frontend/wailsjs/runtime/package.json
Normal file
24
cmd/dapp-fm-app/frontend/wailsjs/runtime/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
249
cmd/dapp-fm-app/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
cmd/dapp-fm-app/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
242
cmd/dapp-fm-app/frontend/wailsjs/runtime/runtime.js
Normal file
242
cmd/dapp-fm-app/frontend/wailsjs/runtime/runtime.js
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
322
cmd/dapp-fm-app/main.go
Normal file
322
cmd/dapp-fm-app/main.go
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
// dapp-fm-app is a native desktop media player for dapp.fm
|
||||
// Decryption in Go, media served via Wails asset handler (same origin, no CORS)
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Snider/Borg/pkg/player"
|
||||
"github.com/Snider/Borg/pkg/smsg"
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed frontend
|
||||
var frontendAssets embed.FS
|
||||
|
||||
// MediaStore holds decrypted media in memory
|
||||
type MediaStore struct {
|
||||
mu sync.RWMutex
|
||||
media map[string]*MediaItem
|
||||
}
|
||||
|
||||
type MediaItem struct {
|
||||
Data []byte
|
||||
MimeType string
|
||||
Name string
|
||||
}
|
||||
|
||||
var globalStore = &MediaStore{media: make(map[string]*MediaItem)}
|
||||
|
||||
func (s *MediaStore) Set(id string, item *MediaItem) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.media[id] = item
|
||||
}
|
||||
|
||||
func (s *MediaStore) Get(id string) *MediaItem {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.media[id]
|
||||
}
|
||||
|
||||
func (s *MediaStore) Clear() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.media = make(map[string]*MediaItem)
|
||||
}
|
||||
|
||||
// AssetHandler serves both static assets and decrypted media
|
||||
type AssetHandler struct {
|
||||
assets fs.FS
|
||||
}
|
||||
|
||||
func (h *AssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
if path == "/" {
|
||||
path = "/index.html"
|
||||
}
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
|
||||
// Check if this is a media request
|
||||
if strings.HasPrefix(path, "media/") {
|
||||
id := strings.TrimPrefix(path, "media/")
|
||||
item := globalStore.Get(id)
|
||||
if item == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Serve with range support for seeking
|
||||
w.Header().Set("Content-Type", item.MimeType)
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(item.Data)))
|
||||
|
||||
rangeHeader := r.Header.Get("Range")
|
||||
if rangeHeader != "" && strings.HasPrefix(rangeHeader, "bytes=") {
|
||||
rangeHeader = strings.TrimPrefix(rangeHeader, "bytes=")
|
||||
parts := strings.Split(rangeHeader, "-")
|
||||
start, _ := strconv.Atoi(parts[0])
|
||||
end := len(item.Data) - 1
|
||||
if len(parts) > 1 && parts[1] != "" {
|
||||
end, _ = strconv.Atoi(parts[1])
|
||||
}
|
||||
if end >= len(item.Data) {
|
||||
end = len(item.Data) - 1
|
||||
}
|
||||
if start > end || start >= len(item.Data) {
|
||||
http.Error(w, "Invalid range", http.StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(item.Data)))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(end-start+1))
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
w.Write(item.Data[start : end+1])
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, item.Name, time.Time{}, bytes.NewReader(item.Data))
|
||||
return
|
||||
}
|
||||
|
||||
// Serve static assets
|
||||
data, err := fs.ReadFile(h.assets, "frontend/"+path)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Set content type
|
||||
switch {
|
||||
case strings.HasSuffix(path, ".html"):
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
case strings.HasSuffix(path, ".js"):
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
case strings.HasSuffix(path, ".css"):
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
case strings.HasSuffix(path, ".wasm"):
|
||||
w.Header().Set("Content-Type", "application/wasm")
|
||||
}
|
||||
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
// App wraps player functionality
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
player *player.Player
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{
|
||||
player: player.NewPlayer(),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
a.player.Startup(ctx)
|
||||
}
|
||||
|
||||
// MediaResult holds URLs for playback
|
||||
type MediaResult struct {
|
||||
Body string `json:"body"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
Attachments []MediaAttachment `json:"attachments,omitempty"`
|
||||
}
|
||||
|
||||
type MediaAttachment struct {
|
||||
Name string `json:"name"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int `json:"size"`
|
||||
URL string `json:"url"` // /media/0, /media/1, etc.
|
||||
}
|
||||
|
||||
// LoadDemo decrypts demo and stores in memory for streaming
|
||||
func (a *App) LoadDemo() (*MediaResult, error) {
|
||||
globalStore.Clear()
|
||||
|
||||
// Read demo from embedded filesystem
|
||||
demoBytes, err := fs.ReadFile(frontendAssets, "frontend/demo-track.smsg")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("demo not found: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
msg, err := smsg.Decrypt(demoBytes, "dapp-fm-2024")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt failed: %w", err)
|
||||
}
|
||||
|
||||
result := &MediaResult{
|
||||
Body: msg.Body,
|
||||
Subject: msg.Subject,
|
||||
From: msg.From,
|
||||
}
|
||||
|
||||
for i, att := range msg.Attachments {
|
||||
// Decode base64 to raw bytes
|
||||
data, err := base64.StdEncoding.DecodeString(att.Content)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Store in memory
|
||||
id := strconv.Itoa(i)
|
||||
globalStore.Set(id, &MediaItem{
|
||||
Data: data,
|
||||
MimeType: att.MimeType,
|
||||
Name: att.Name,
|
||||
})
|
||||
|
||||
result.Attachments = append(result.Attachments, MediaAttachment{
|
||||
Name: att.Name,
|
||||
MimeType: att.MimeType,
|
||||
Size: len(data),
|
||||
URL: "/media/" + id,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetDemoManifest returns manifest without decrypting
|
||||
func (a *App) GetDemoManifest() (*player.ManifestInfo, error) {
|
||||
demoBytes, err := fs.ReadFile(frontendAssets, "frontend/demo-track.smsg")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("demo not found: %w", err)
|
||||
}
|
||||
|
||||
info, err := smsg.GetInfo(demoBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &player.ManifestInfo{}
|
||||
if info.Manifest != nil {
|
||||
m := info.Manifest
|
||||
result.Title = m.Title
|
||||
result.Artist = m.Artist
|
||||
result.Album = m.Album
|
||||
result.ReleaseType = m.ReleaseType
|
||||
result.Format = m.Format
|
||||
result.LicenseType = m.LicenseType
|
||||
|
||||
for _, t := range m.Tracks {
|
||||
result.Tracks = append(result.Tracks, player.TrackInfo{
|
||||
Title: t.Title,
|
||||
Start: t.Start,
|
||||
End: t.End,
|
||||
TrackNum: t.TrackNum,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecryptAndServe decrypts user-provided content and serves via asset handler
|
||||
func (a *App) DecryptAndServe(encrypted string, password string) (*MediaResult, error) {
|
||||
globalStore.Clear()
|
||||
|
||||
// Decrypt using player (handles base64 input)
|
||||
msg, err := smsg.DecryptBase64(encrypted, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt failed: %w", err)
|
||||
}
|
||||
|
||||
result := &MediaResult{
|
||||
Body: msg.Body,
|
||||
Subject: msg.Subject,
|
||||
From: msg.From,
|
||||
}
|
||||
|
||||
for i, att := range msg.Attachments {
|
||||
data, err := base64.StdEncoding.DecodeString(att.Content)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
id := strconv.Itoa(i)
|
||||
globalStore.Set(id, &MediaItem{
|
||||
Data: data,
|
||||
MimeType: att.MimeType,
|
||||
Name: att.Name,
|
||||
})
|
||||
|
||||
result.Attachments = append(result.Attachments, MediaAttachment{
|
||||
Name: att.Name,
|
||||
MimeType: att.MimeType,
|
||||
Size: len(data),
|
||||
URL: "/media/" + id,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Proxy methods
|
||||
func (a *App) GetManifest(encrypted string) (*player.ManifestInfo, error) {
|
||||
return a.player.GetManifest(encrypted)
|
||||
}
|
||||
|
||||
func (a *App) IsLicenseValid(encrypted string) (bool, error) {
|
||||
return a.player.IsLicenseValid(encrypted)
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := NewApp()
|
||||
|
||||
err := wails.Run(&options.App{
|
||||
Title: "dapp.fm Player",
|
||||
Width: 1200,
|
||||
Height: 800,
|
||||
MinWidth: 800,
|
||||
MinHeight: 600,
|
||||
AssetServer: &assetserver.Options{
|
||||
Handler: &AssetHandler{assets: frontendAssets},
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 18, G: 18, B: 18, A: 1},
|
||||
OnStartup: app.Startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
20
cmd/dapp-fm-app/wails.json
Normal file
20
cmd/dapp-fm-app/wails.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "dapp-fm",
|
||||
"outputfilename": "dapp-fm",
|
||||
"frontend:install": "",
|
||||
"frontend:build": "",
|
||||
"frontend:dev:watcher": "",
|
||||
"frontend:dev:serverUrl": "",
|
||||
"author": {
|
||||
"name": "dapp.fm",
|
||||
"email": "hello@dapp.fm"
|
||||
},
|
||||
"info": {
|
||||
"companyName": "dapp.fm",
|
||||
"productName": "dapp.fm Player",
|
||||
"productVersion": "1.0.0",
|
||||
"copyright": "Copyright (c) 2024 dapp.fm - EUPL-1.2",
|
||||
"comments": "Decentralized Music Distribution - Zero-Trust DRM"
|
||||
}
|
||||
}
|
||||
64
cmd/dapp-fm/main.go
Normal file
64
cmd/dapp-fm/main.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// dapp-fm CLI provides headless media player functionality
|
||||
// For native desktop app with WebView, use dapp-fm-app instead
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Snider/Borg/pkg/player"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "dapp-fm",
|
||||
Short: "dapp.fm - Decentralized Music Player CLI",
|
||||
Long: `dapp-fm is the CLI version of the dapp.fm player.
|
||||
|
||||
For the native desktop app with WebView, use dapp-fm-app instead.
|
||||
This CLI provides HTTP server mode for automation and fallback scenarios.`,
|
||||
}
|
||||
|
||||
serveCmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start HTTP server for the media player",
|
||||
Long: `Starts an HTTP server serving the media player interface.
|
||||
This is the slower TCP path - for memory-speed decryption, use dapp-fm-app.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
port, _ := cmd.Flags().GetString("port")
|
||||
openBrowser, _ := cmd.Flags().GetBool("open")
|
||||
|
||||
p := player.NewPlayer()
|
||||
|
||||
addr := ":" + port
|
||||
if openBrowser {
|
||||
fmt.Printf("Opening browser at http://localhost%s\n", addr)
|
||||
// Would need browser opener here
|
||||
}
|
||||
|
||||
return p.Serve(addr)
|
||||
},
|
||||
}
|
||||
|
||||
serveCmd.Flags().StringP("port", "p", "8080", "Port to serve on")
|
||||
serveCmd.Flags().Bool("open", false, "Open browser automatically")
|
||||
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("dapp-fm v1.0.0")
|
||||
fmt.Println("Decentralized Music Distribution")
|
||||
fmt.Println("https://dapp.fm")
|
||||
},
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
88
cmd/decode.go
Normal file
88
cmd/decode.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
trixsdk "github.com/Snider/Enchantrix/pkg/trix"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var decodeCmd = NewDecodeCmd()
|
||||
|
||||
func NewDecodeCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "decode [file]",
|
||||
Short: "Decode a .trix, .tim, or .stim file",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inputFile := args[0]
|
||||
outputFile, _ := cmd.Flags().GetString("output")
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
inIsolation, _ := cmd.Flags().GetBool("i-am-in-isolation")
|
||||
|
||||
data, err := os.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if it's a .stim file (encrypted TIM)
|
||||
if strings.HasSuffix(inputFile, ".stim") || (len(data) >= 4 && string(data[:4]) == "STIM") {
|
||||
if password == "" {
|
||||
return fmt.Errorf("password required for .stim files")
|
||||
}
|
||||
if !inIsolation {
|
||||
return fmt.Errorf("this is an encrypted Terminal Isolation Matrix, use the --i-am-in-isolation flag to decode it")
|
||||
}
|
||||
m, err := tim.FromSigil(data, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tarball, err := m.ToTar()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Decoded encrypted TIM to %s\n", outputFile)
|
||||
return os.WriteFile(outputFile, tarball, 0644)
|
||||
}
|
||||
|
||||
// Try TRIX format
|
||||
t, err := trixsdk.Decode(data, "TRIX", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := t.Header["tim"]; ok && !inIsolation {
|
||||
return fmt.Errorf("this is a Terminal Isolation Matrix, use the --i-am-in-isolation flag to decode it")
|
||||
}
|
||||
|
||||
dn, err := trix.FromTrix(data, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tarball, err := dn.ToTar()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Decoded to %s\n", outputFile)
|
||||
return os.WriteFile(outputFile, tarball, 0644)
|
||||
},
|
||||
}
|
||||
cmd.Flags().String("output", "decoded.dat", "Output file for the decoded data")
|
||||
cmd.Flags().String("password", "", "Password for decryption")
|
||||
cmd.Flags().Bool("i-am-in-isolation", false, "Required to decode a Terminal Isolation Matrix")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func GetDecodeCmd() *cobra.Command {
|
||||
return decodeCmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(GetDecodeCmd())
|
||||
}
|
||||
44
cmd/decode_test.go
Normal file
44
cmd/decode_test.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
)
|
||||
|
||||
func TestDecodeCmd(t *testing.T) {
|
||||
t.Run("Good", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
outputFile := filepath.Join(tempDir, "decoded.dat")
|
||||
inputFile := filepath.Join(tempDir, "test.trix")
|
||||
|
||||
// Create a dummy trix file.
|
||||
dn := datanode.New()
|
||||
dn.AddData("test.txt", []byte("hello"))
|
||||
trixBytes, err := trix.ToTrix(dn, "")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create trix file: %v", err)
|
||||
}
|
||||
err = os.WriteFile(inputFile, trixBytes, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write trix file: %v", err)
|
||||
}
|
||||
|
||||
// Execute the decode command.
|
||||
cmd := NewDecodeCmd()
|
||||
cmd.SetArgs([]string{inputFile, "--output", outputFile})
|
||||
err = cmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("decode command failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the output file.
|
||||
_, err = os.Stat(outputFile)
|
||||
if err != nil {
|
||||
t.Fatalf("output file not found: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
5
cmd/exec.go
Normal file
5
cmd/exec.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package cmd
|
||||
|
||||
import "os/exec"
|
||||
|
||||
var execCommand = exec.Command
|
||||
70
cmd/extract-demo/main.go
Normal file
70
cmd/extract-demo/main.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// extract-demo extracts the video from a v2 SMSG file
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Snider/Borg/pkg/smsg"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Println("Usage: extract-demo <input.smsg> <password> <output.mp4>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
inputFile := os.Args[1]
|
||||
password := os.Args[2]
|
||||
outputFile := os.Args[3]
|
||||
|
||||
data, err := os.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get info first
|
||||
info, err := smsg.GetInfo(data)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get info: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Format: %s, Compression: %s\n", info.Format, info.Compression)
|
||||
|
||||
// Decrypt
|
||||
msg, err := smsg.Decrypt(data, password)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to decrypt: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Body: %s...\n", msg.Body[:min(50, len(msg.Body))])
|
||||
fmt.Printf("Attachments: %d\n", len(msg.Attachments))
|
||||
|
||||
if len(msg.Attachments) > 0 {
|
||||
att := msg.Attachments[0]
|
||||
fmt.Printf(" Name: %s, MIME: %s, Size: %d\n", att.Name, att.MimeType, att.Size)
|
||||
|
||||
// Decode and save
|
||||
decoded, err := base64.StdEncoding.DecodeString(att.Content)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to decode: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outputFile, decoded, 0644); err != nil {
|
||||
fmt.Printf("Failed to save: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Saved to %s (%d bytes)\n", outputFile, len(decoded))
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
114
cmd/inspect.go
Normal file
114
cmd/inspect.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
trixsdk "github.com/Snider/Enchantrix/pkg/trix"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var inspectCmd = NewInspectCmd()
|
||||
|
||||
func NewInspectCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [file]",
|
||||
Short: "Inspect metadata of a .trix or .stim file without decrypting",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inputFile := args[0]
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
data, err := os.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(data) < 4 {
|
||||
return fmt.Errorf("file too small to be a valid container")
|
||||
}
|
||||
|
||||
magic := string(data[:4])
|
||||
var t *trixsdk.Trix
|
||||
|
||||
switch magic {
|
||||
case "STIM":
|
||||
t, err = trixsdk.Decode(data, "STIM", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode STIM: %w", err)
|
||||
}
|
||||
case "TRIX":
|
||||
t, err = trixsdk.Decode(data, "TRIX", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode TRIX: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown file format (magic: %q)", magic)
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
info := map[string]interface{}{
|
||||
"file": inputFile,
|
||||
"magic": magic,
|
||||
"header": t.Header,
|
||||
"payload_size": len(t.Payload),
|
||||
}
|
||||
enc := json.NewEncoder(cmd.OutOrStdout())
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(info)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "File: %s\n", inputFile)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Format: %s\n", magic)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Payload Size: %d bytes\n", len(t.Payload))
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Header:\n")
|
||||
|
||||
for k, v := range t.Header {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), " %s: %v\n", k, v)
|
||||
}
|
||||
|
||||
// Show encryption info
|
||||
if algo, ok := t.Header["encryption_algorithm"]; ok {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "\nEncryption: %v\n", algo)
|
||||
}
|
||||
if _, ok := t.Header["tim"]; ok {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Type: Terminal Isolation Matrix\n")
|
||||
}
|
||||
if v, ok := t.Header["version"]; ok {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Version: %v\n", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().Bool("json", false, "Output in JSON format")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func GetInspectCmd() *cobra.Command {
|
||||
return inspectCmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(GetInspectCmd())
|
||||
}
|
||||
|
||||
// isStimFile checks if a file is a .stim file by extension or magic number.
|
||||
func isStimFile(path string) bool {
|
||||
if strings.HasSuffix(path, ".stim") {
|
||||
return true
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
magic := make([]byte, 4)
|
||||
if _, err := f.Read(magic); err != nil {
|
||||
return false
|
||||
}
|
||||
return string(magic) == "STIM"
|
||||
}
|
||||
226
cmd/mkdemo-abr/main.go
Normal file
226
cmd/mkdemo-abr/main.go
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
// mkdemo-abr creates an ABR (Adaptive Bitrate) demo set from a source video.
|
||||
// It uses ffmpeg to transcode to multiple bitrates, then encrypts each as v3 chunked SMSG.
|
||||
//
|
||||
// Usage: mkdemo-abr <input-video> <output-dir> [password]
|
||||
//
|
||||
// Output:
|
||||
//
|
||||
// output-dir/manifest.json - ABR manifest listing all variants
|
||||
// output-dir/track-1080p.smsg - 1080p variant (5 Mbps)
|
||||
// output-dir/track-720p.smsg - 720p variant (2.5 Mbps)
|
||||
// output-dir/track-480p.smsg - 480p variant (1 Mbps)
|
||||
// output-dir/track-360p.smsg - 360p variant (500 Kbps)
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Borg/pkg/smsg"
|
||||
)
|
||||
|
||||
// Preset defines a quality level for transcoding
|
||||
type Preset struct {
|
||||
Name string
|
||||
Width int
|
||||
Height int
|
||||
Bitrate string // For ffmpeg (e.g., "5M")
|
||||
BPS int // Bits per second for manifest
|
||||
}
|
||||
|
||||
// Default presets matching ABRPresets in types.go
|
||||
var presets = []Preset{
|
||||
{"1080p", 1920, 1080, "5M", 5000000},
|
||||
{"720p", 1280, 720, "2.5M", 2500000},
|
||||
{"480p", 854, 480, "1M", 1000000},
|
||||
{"360p", 640, 360, "500K", 500000},
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("Usage: mkdemo-abr <input-video> <output-dir> [password]")
|
||||
fmt.Println()
|
||||
fmt.Println("Creates ABR variant set from source video using ffmpeg.")
|
||||
fmt.Println()
|
||||
fmt.Println("Output:")
|
||||
fmt.Println(" output-dir/manifest.json - ABR manifest")
|
||||
fmt.Println(" output-dir/track-1080p.smsg - 1080p (5 Mbps)")
|
||||
fmt.Println(" output-dir/track-720p.smsg - 720p (2.5 Mbps)")
|
||||
fmt.Println(" output-dir/track-480p.smsg - 480p (1 Mbps)")
|
||||
fmt.Println(" output-dir/track-360p.smsg - 360p (500 Kbps)")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
inputFile := os.Args[1]
|
||||
outputDir := os.Args[2]
|
||||
|
||||
// Check ffmpeg is available
|
||||
if _, err := exec.LookPath("ffmpeg"); err != nil {
|
||||
fmt.Println("Error: ffmpeg not found in PATH")
|
||||
fmt.Println("Install ffmpeg: https://ffmpeg.org/download.html")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Generate or use provided password
|
||||
var password string
|
||||
if len(os.Args) > 3 {
|
||||
password = os.Args[3]
|
||||
} else {
|
||||
passwordBytes := make([]byte, 24)
|
||||
if _, err := rand.Read(passwordBytes); err != nil {
|
||||
fmt.Printf("Failed to generate password: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
password = base64.RawURLEncoding.EncodeToString(passwordBytes)
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
fmt.Printf("Failed to create output directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get title from input filename
|
||||
title := filepath.Base(inputFile)
|
||||
ext := filepath.Ext(title)
|
||||
if ext != "" {
|
||||
title = title[:len(title)-len(ext)]
|
||||
}
|
||||
|
||||
// Create ABR manifest
|
||||
manifest := smsg.NewABRManifest(title)
|
||||
|
||||
fmt.Printf("Creating ABR variants for: %s\n", inputFile)
|
||||
fmt.Printf("Output directory: %s\n", outputDir)
|
||||
fmt.Printf("Password: %s\n\n", password)
|
||||
|
||||
// Process each preset
|
||||
for _, preset := range presets {
|
||||
fmt.Printf("Processing %s (%dx%d @ %s)...\n", preset.Name, preset.Width, preset.Height, preset.Bitrate)
|
||||
|
||||
// Step 1: Transcode with ffmpeg
|
||||
tempFile := filepath.Join(outputDir, fmt.Sprintf("temp-%s.mp4", preset.Name))
|
||||
if err := transcode(inputFile, tempFile, preset); err != nil {
|
||||
fmt.Printf(" Warning: Transcode failed for %s: %v\n", preset.Name, err)
|
||||
fmt.Printf(" Skipping this variant...\n")
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 2: Read transcoded file
|
||||
content, err := os.ReadFile(tempFile)
|
||||
if err != nil {
|
||||
fmt.Printf(" Error reading transcoded file: %v\n", err)
|
||||
os.Remove(tempFile)
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 3: Create SMSG message
|
||||
msg := smsg.NewMessage("dapp.fm ABR Demo")
|
||||
msg.Subject = fmt.Sprintf("%s - %s", title, preset.Name)
|
||||
msg.From = "dapp.fm"
|
||||
msg.AddBinaryAttachment(
|
||||
fmt.Sprintf("%s-%s.mp4", strings.ReplaceAll(title, " ", "_"), preset.Name),
|
||||
content,
|
||||
"video/mp4",
|
||||
)
|
||||
|
||||
// Step 4: Create manifest for this variant
|
||||
variantManifest := smsg.NewManifest(title)
|
||||
variantManifest.LicenseType = "perpetual"
|
||||
variantManifest.Format = "dapp.fm/abr-v1"
|
||||
|
||||
// Step 5: Encrypt with v3 chunked format
|
||||
params := &smsg.StreamParams{
|
||||
License: password,
|
||||
ChunkSize: smsg.DefaultChunkSize, // 1MB chunks
|
||||
}
|
||||
|
||||
encrypted, err := smsg.EncryptV3(msg, params, variantManifest)
|
||||
if err != nil {
|
||||
fmt.Printf(" Error encrypting: %v\n", err)
|
||||
os.Remove(tempFile)
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 6: Write SMSG file
|
||||
smsgFile := filepath.Join(outputDir, fmt.Sprintf("track-%s.smsg", preset.Name))
|
||||
if err := os.WriteFile(smsgFile, encrypted, 0644); err != nil {
|
||||
fmt.Printf(" Error writing SMSG: %v\n", err)
|
||||
os.Remove(tempFile)
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 7: Get chunk count from header
|
||||
header, err := smsg.GetV3Header(encrypted)
|
||||
if err != nil {
|
||||
fmt.Printf(" Warning: Could not read header: %v\n", err)
|
||||
}
|
||||
chunkCount := 0
|
||||
if header != nil && header.Chunked != nil {
|
||||
chunkCount = header.Chunked.TotalChunks
|
||||
}
|
||||
|
||||
// Step 8: Add variant to manifest
|
||||
variant := smsg.Variant{
|
||||
Name: preset.Name,
|
||||
Bandwidth: preset.BPS,
|
||||
Width: preset.Width,
|
||||
Height: preset.Height,
|
||||
Codecs: "avc1.640028,mp4a.40.2",
|
||||
URL: fmt.Sprintf("track-%s.smsg", preset.Name),
|
||||
ChunkCount: chunkCount,
|
||||
FileSize: int64(len(encrypted)),
|
||||
}
|
||||
manifest.AddVariant(variant)
|
||||
|
||||
// Clean up temp file
|
||||
os.Remove(tempFile)
|
||||
|
||||
fmt.Printf(" Created: %s (%d bytes, %d chunks)\n", smsgFile, len(encrypted), chunkCount)
|
||||
}
|
||||
|
||||
if len(manifest.Variants) == 0 {
|
||||
fmt.Println("\nError: No variants created. Check ffmpeg output.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Write ABR manifest
|
||||
manifestPath := filepath.Join(outputDir, "manifest.json")
|
||||
if err := smsg.WriteABRManifest(manifest, manifestPath); err != nil {
|
||||
fmt.Printf("Failed to write manifest: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("\n✓ Created ABR manifest: %s\n", manifestPath)
|
||||
fmt.Printf("✓ Variants: %d\n", len(manifest.Variants))
|
||||
fmt.Printf("✓ Default: %s\n", manifest.Variants[manifest.DefaultIdx].Name)
|
||||
fmt.Printf("\nMaster Password: %s\n", password)
|
||||
fmt.Println("\nStore this password securely - it decrypts ALL variants!")
|
||||
}
|
||||
|
||||
// transcode uses ffmpeg to transcode the input to the specified preset
|
||||
func transcode(input, output string, preset Preset) error {
|
||||
args := []string{
|
||||
"-i", input,
|
||||
"-vf", fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2",
|
||||
preset.Width, preset.Height, preset.Width, preset.Height),
|
||||
"-c:v", "libx264",
|
||||
"-preset", "medium",
|
||||
"-b:v", preset.Bitrate,
|
||||
"-c:a", "aac",
|
||||
"-b:a", "128k",
|
||||
"-movflags", "+faststart",
|
||||
"-y", // Overwrite output
|
||||
output,
|
||||
}
|
||||
|
||||
cmd := exec.Command("ffmpeg", args...)
|
||||
cmd.Stderr = os.Stderr // Show ffmpeg output for debugging
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
129
cmd/mkdemo-v3/main.go
Normal file
129
cmd/mkdemo-v3/main.go
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// mkdemo-v3 creates a v3 chunked SMSG file for streaming demos
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Snider/Borg/pkg/smsg"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("Usage: mkdemo-v3 <input-media-file> <output-smsg-file> [license] [chunk-size-kb]")
|
||||
fmt.Println("")
|
||||
fmt.Println("Creates a v3 chunked SMSG file for streaming demos.")
|
||||
fmt.Println("V3 uses rolling keys derived from: LTHN(date:license:fingerprint)")
|
||||
fmt.Println("")
|
||||
fmt.Println("Options:")
|
||||
fmt.Println(" license The license key (default: auto-generated)")
|
||||
fmt.Println(" chunk-size-kb Chunk size in KB (default: 512)")
|
||||
fmt.Println("")
|
||||
fmt.Println("Note: V3 files work for 24-48 hours from creation (rolling keys).")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
inputFile := os.Args[1]
|
||||
outputFile := os.Args[2]
|
||||
|
||||
// Read input file
|
||||
content, err := os.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read input file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// License (acts as password in v3)
|
||||
var license string
|
||||
if len(os.Args) > 3 {
|
||||
license = os.Args[3]
|
||||
} else {
|
||||
// Generate cryptographically secure license
|
||||
licenseBytes := make([]byte, 24)
|
||||
if _, err := rand.Read(licenseBytes); err != nil {
|
||||
fmt.Printf("Failed to generate license: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
license = base64.RawURLEncoding.EncodeToString(licenseBytes)
|
||||
}
|
||||
|
||||
// Chunk size (default 512KB for good streaming granularity)
|
||||
chunkSize := 512 * 1024
|
||||
if len(os.Args) > 4 {
|
||||
var chunkKB int
|
||||
if _, err := fmt.Sscanf(os.Args[4], "%d", &chunkKB); err == nil && chunkKB > 0 {
|
||||
chunkSize = chunkKB * 1024
|
||||
}
|
||||
}
|
||||
|
||||
// Create manifest
|
||||
title := filepath.Base(inputFile)
|
||||
ext := filepath.Ext(title)
|
||||
if ext != "" {
|
||||
title = title[:len(title)-len(ext)]
|
||||
}
|
||||
manifest := smsg.NewManifest(title)
|
||||
manifest.LicenseType = "streaming"
|
||||
manifest.Format = "dapp.fm/v3-chunked"
|
||||
|
||||
// Detect MIME type
|
||||
mimeType := "video/mp4"
|
||||
switch ext {
|
||||
case ".mp3":
|
||||
mimeType = "audio/mpeg"
|
||||
case ".wav":
|
||||
mimeType = "audio/wav"
|
||||
case ".flac":
|
||||
mimeType = "audio/flac"
|
||||
case ".webm":
|
||||
mimeType = "video/webm"
|
||||
case ".ogg":
|
||||
mimeType = "audio/ogg"
|
||||
}
|
||||
|
||||
// Create message with attachment
|
||||
msg := smsg.NewMessage("dapp.fm V3 Streaming Demo - Decrypt-while-downloading enabled")
|
||||
msg.Subject = "V3 Chunked Streaming"
|
||||
msg.From = "dapp.fm"
|
||||
msg.AddBinaryAttachment(
|
||||
filepath.Base(inputFile),
|
||||
content,
|
||||
mimeType,
|
||||
)
|
||||
|
||||
// Create stream params with chunking enabled
|
||||
params := &smsg.StreamParams{
|
||||
License: license,
|
||||
Fingerprint: "", // Empty for demo (works for any device)
|
||||
Cadence: smsg.CadenceDaily,
|
||||
ChunkSize: chunkSize,
|
||||
}
|
||||
|
||||
// Encrypt with v3 chunked format
|
||||
encrypted, err := smsg.EncryptV3(msg, params, manifest)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to encrypt: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Write output
|
||||
if err := os.WriteFile(outputFile, encrypted, 0644); err != nil {
|
||||
fmt.Printf("Failed to write output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Calculate chunk count
|
||||
numChunks := (len(content) + chunkSize - 1) / chunkSize
|
||||
|
||||
fmt.Printf("Created: %s (%d bytes)\n", outputFile, len(encrypted))
|
||||
fmt.Printf("Format: v3 chunked\n")
|
||||
fmt.Printf("Chunk Size: %d KB\n", chunkSize/1024)
|
||||
fmt.Printf("Total Chunks: ~%d\n", numChunks)
|
||||
fmt.Printf("License: %s\n", license)
|
||||
fmt.Println("")
|
||||
fmt.Println("This license works for 24-48 hours from creation.")
|
||||
fmt.Println("Use the license in the streaming demo to decrypt.")
|
||||
}
|
||||
81
cmd/mkdemo/main.go
Normal file
81
cmd/mkdemo/main.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// mkdemo creates an RFC-quality demo SMSG file with a cryptographically secure password
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Snider/Borg/pkg/smsg"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("Usage: mkdemo <input-media-file> <output-smsg-file>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
inputFile := os.Args[1]
|
||||
outputFile := os.Args[2]
|
||||
|
||||
// Read input file
|
||||
content, err := os.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read input file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Use existing password or generate new one
|
||||
var password string
|
||||
if len(os.Args) > 3 {
|
||||
password = os.Args[3]
|
||||
} else {
|
||||
// Generate cryptographically secure password (32 bytes = 256 bits)
|
||||
passwordBytes := make([]byte, 24)
|
||||
if _, err := rand.Read(passwordBytes); err != nil {
|
||||
fmt.Printf("Failed to generate password: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Use base64url encoding, trimmed to 32 chars for readability
|
||||
password = base64.RawURLEncoding.EncodeToString(passwordBytes)
|
||||
}
|
||||
|
||||
// Create manifest with filename as title
|
||||
title := filepath.Base(inputFile)
|
||||
ext := filepath.Ext(title)
|
||||
if ext != "" {
|
||||
title = title[:len(title)-len(ext)]
|
||||
}
|
||||
manifest := smsg.NewManifest(title)
|
||||
manifest.LicenseType = "perpetual"
|
||||
manifest.Format = "dapp.fm/v1"
|
||||
|
||||
// Create message with attachment (using binary attachment for v2 format)
|
||||
msg := smsg.NewMessage("Welcome to dapp.fm - Zero-Trust DRM for the open web.")
|
||||
msg.Subject = "dapp.fm Demo"
|
||||
msg.From = "dapp.fm"
|
||||
msg.AddBinaryAttachment(
|
||||
filepath.Base(inputFile),
|
||||
content,
|
||||
"video/mp4",
|
||||
)
|
||||
|
||||
// Encrypt with v2 binary format (smaller file size)
|
||||
encrypted, err := smsg.EncryptV2WithManifest(msg, password, manifest)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to encrypt: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Write output
|
||||
if err := os.WriteFile(outputFile, encrypted, 0644); err != nil {
|
||||
fmt.Printf("Failed to write output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Created: %s (%d bytes)\n", outputFile, len(encrypted))
|
||||
fmt.Printf("Master Password: %s\n", password)
|
||||
fmt.Println("\nStore this password securely - it cannot be recovered!")
|
||||
}
|
||||
30
cmd/root.go
Normal file
30
cmd/root.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewRootCmd() *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "borg",
|
||||
Short: "A tool for collecting and managing data.",
|
||||
Long: `Borg Data Collector is a command-line tool for cloning Git repositories,
|
||||
packaging their contents into a single file, and managing the data within.`,
|
||||
}
|
||||
|
||||
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging")
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
// RootCmd represents the base command when called without any subcommands
|
||||
var RootCmd = NewRootCmd()
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute(log *slog.Logger) error {
|
||||
RootCmd.SetContext(context.WithValue(context.Background(), "logger", log))
|
||||
return RootCmd.Execute()
|
||||
}
|
||||
84
cmd/root_test.go
Normal file
84
cmd/root_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// executeCommand is a helper function to execute a cobra command and return the output.
|
||||
func executeCommand(root *cobra.Command, args ...string) (string, error) {
|
||||
_, output, err := executeCommandC(root, args...)
|
||||
return output, err
|
||||
}
|
||||
|
||||
// executeCommandC is a helper function to execute a cobra command and return the output.
|
||||
func executeCommandC(root *cobra.Command, args ...string) (*cobra.Command, string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
root.SetOut(buf)
|
||||
root.SetErr(buf)
|
||||
root.SetArgs(args)
|
||||
|
||||
c, err := root.ExecuteC()
|
||||
|
||||
return c, buf.String(), err
|
||||
}
|
||||
|
||||
func TestExecute_Good(t *testing.T) {
|
||||
// This is a basic test to ensure the command runs without panicking.
|
||||
err := Execute(slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootCmd_Good(t *testing.T) {
|
||||
t.Run("No args", func(t *testing.T) {
|
||||
_, err := executeCommand(RootCmd)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Help flag", func(t *testing.T) {
|
||||
// We need to reset the command's state before each run.
|
||||
RootCmd.ResetFlags()
|
||||
RootCmd.ResetCommands()
|
||||
initAllCommands()
|
||||
|
||||
output, err := executeCommand(RootCmd, "--help")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(output, "Usage:") {
|
||||
t.Errorf("expected help output to contain 'Usage:', but it did not")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRootCmd_Bad(t *testing.T) {
|
||||
t.Run("Unknown command", func(t *testing.T) {
|
||||
// We need to reset the command's state before each run.
|
||||
RootCmd.ResetFlags()
|
||||
RootCmd.ResetCommands()
|
||||
initAllCommands()
|
||||
|
||||
_, err := executeCommand(RootCmd, "unknown-command")
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for an unknown command, but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// initAllCommands re-initializes all commands for testing.
|
||||
func initAllCommands() {
|
||||
RootCmd.AddCommand(GetAllCmd())
|
||||
RootCmd.AddCommand(GetCollectCmd())
|
||||
RootCmd.AddCommand(GetCompileCmd())
|
||||
RootCmd.AddCommand(GetRunCmd())
|
||||
RootCmd.AddCommand(GetServeCmd())
|
||||
}
|
||||
63
cmd/run.go
Normal file
63
cmd/run.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var runPassword string
|
||||
|
||||
var runCmd = NewRunCmd()
|
||||
|
||||
func NewRunCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "run [tim file]",
|
||||
Short: "Run a Terminal Isolation Matrix.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
filePath := args[0]
|
||||
|
||||
// Check if encrypted by extension or magic number
|
||||
if isEncryptedTIM(filePath) {
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
if password == "" {
|
||||
return tim.ErrPasswordRequired
|
||||
}
|
||||
return tim.RunEncrypted(filePath, password)
|
||||
}
|
||||
|
||||
return tim.Run(filePath)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&runPassword, "password", "p", "", "Decryption password for encrypted TIMs (.stim)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// isEncryptedTIM checks if a file is an encrypted TIM by extension or magic number.
|
||||
func isEncryptedTIM(path string) bool {
|
||||
if strings.HasSuffix(path, ".stim") {
|
||||
return true
|
||||
}
|
||||
// Check magic number
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
magic := make([]byte, 4)
|
||||
if _, err := f.Read(magic); err != nil {
|
||||
return false
|
||||
}
|
||||
return string(magic) == "STIM"
|
||||
}
|
||||
|
||||
func GetRunCmd() *cobra.Command {
|
||||
return runCmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(GetRunCmd())
|
||||
}
|
||||
122
cmd/run_test.go
Normal file
122
cmd/run_test.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
)
|
||||
|
||||
func TestRunCmd_Good(t *testing.T) {
|
||||
// Create a dummy tim file.
|
||||
timPath := createDummyTim(t)
|
||||
|
||||
// Mock the exec.Command function in the tim package.
|
||||
origExecCommand := tim.ExecCommand
|
||||
tim.ExecCommand = func(command string, args ...string) *exec.Cmd {
|
||||
cs := []string{"-test.run=TestHelperProcess", "--", command}
|
||||
cs = append(cs, args...)
|
||||
cmd := exec.Command(os.Args[0], cs...)
|
||||
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
|
||||
return cmd
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
tim.ExecCommand = origExecCommand
|
||||
})
|
||||
|
||||
// Run the run command.
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetRunCmd())
|
||||
_, err := executeCommand(rootCmd, "run", timPath)
|
||||
if err != nil {
|
||||
t.Fatalf("run command failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCmd_Bad(t *testing.T) {
|
||||
t.Run("Missing input file", func(t *testing.T) {
|
||||
// Run the run command with a non-existent file.
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetRunCmd())
|
||||
_, err := executeCommand(rootCmd, "run", "/non/existent/file.tim")
|
||||
if err == nil {
|
||||
t.Fatal("run command should have failed but did not")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunCmd_Ugly(t *testing.T) {
|
||||
t.Run("Invalid tim file", func(t *testing.T) {
|
||||
// Create an invalid (non-tar) tim file.
|
||||
tempDir := t.TempDir()
|
||||
timPath := filepath.Join(tempDir, "invalid.tim")
|
||||
err := os.WriteFile(timPath, []byte("this is not a tar file"), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create invalid tim file: %v", err)
|
||||
}
|
||||
|
||||
// Run the run command.
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(GetRunCmd())
|
||||
_, err = executeCommand(rootCmd, "run", timPath)
|
||||
if err == nil {
|
||||
t.Fatal("run command should have failed but did not")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// createDummyTim creates a valid, empty tim file for testing.
|
||||
func createDummyTim(t *testing.T) string {
|
||||
t.Helper()
|
||||
tempDir := t.TempDir()
|
||||
timPath := filepath.Join(tempDir, "test.tim")
|
||||
timFile, err := os.Create(timPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create dummy tim file: %v", err)
|
||||
}
|
||||
defer timFile.Close()
|
||||
|
||||
tw := tar.NewWriter(timFile)
|
||||
|
||||
// Add a dummy config.json. This is not a valid config, but it's enough to test the run command.
|
||||
configContent := []byte(`{}`)
|
||||
hdr := &tar.Header{
|
||||
Name: "config.json",
|
||||
Mode: 0600,
|
||||
Size: int64(len(configContent)),
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
t.Fatalf("failed to write tar header: %v", err)
|
||||
}
|
||||
if _, err := tw.Write(configContent); err != nil {
|
||||
t.Fatalf("failed to write tar content: %v", err)
|
||||
}
|
||||
|
||||
// Add the rootfs directory.
|
||||
hdr = &tar.Header{
|
||||
Name: "rootfs/",
|
||||
Mode: 0755,
|
||||
Typeflag: tar.TypeDir,
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
t.Fatalf("failed to write tar header: %v", err)
|
||||
}
|
||||
|
||||
if err := tw.Close(); err != nil {
|
||||
t.Fatalf("failed to close tar writer: %v", err)
|
||||
}
|
||||
return timPath
|
||||
}
|
||||
|
||||
// TestHelperProcess isn't a real test. It's used as a helper for tests that need to mock exec.Command.
|
||||
func TestHelperProcess(t *testing.T) {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
}
|
||||
// The rest of the arguments are the command and its arguments.
|
||||
// In our case, we don't need to do anything with them.
|
||||
os.Exit(0)
|
||||
}
|
||||
73
cmd/serve.go
Normal file
73
cmd/serve.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/Snider/Borg/pkg/tarfs"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// serveCmd represents the serve command
|
||||
var serveCmd = NewServeCmd()
|
||||
|
||||
func NewServeCmd() *cobra.Command {
|
||||
serveCmd := &cobra.Command{
|
||||
Use: "serve [file]",
|
||||
Short: "Serve a packaged PWA file",
|
||||
Long: `Serves the contents of a packaged PWA file using a static file server.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dataFile := args[0]
|
||||
port, _ := cmd.Flags().GetString("port")
|
||||
|
||||
rawData, err := os.ReadFile(dataFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading data file: %w", err)
|
||||
}
|
||||
|
||||
data, err := compress.Decompress(rawData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error decompressing data: %w", err)
|
||||
}
|
||||
|
||||
var fs http.FileSystem
|
||||
if strings.HasSuffix(dataFile, ".matrix") {
|
||||
fs, err = tarfs.New(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating TarFS from matrix tarball: %w", err)
|
||||
}
|
||||
} else {
|
||||
dn, err := datanode.FromTar(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating DataNode from tarball: %w", err)
|
||||
}
|
||||
fs = http.FS(dn)
|
||||
}
|
||||
|
||||
http.Handle("/", http.FileServer(fs))
|
||||
|
||||
fmt.Printf("Serving PWA on http://localhost:%s\n", port)
|
||||
err = http.ListenAndServe(":"+port, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error starting server: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
serveCmd.PersistentFlags().String("port", "8080", "Port to serve the PWA on")
|
||||
return serveCmd
|
||||
}
|
||||
|
||||
func GetServeCmd() *cobra.Command {
|
||||
return serveCmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(GetServeCmd())
|
||||
}
|
||||
BIN
console.stim
Normal file
BIN
console.stim
Normal file
Binary file not shown.
BIN
demo/demo-sample.smsg
Normal file
BIN
demo/demo-sample.smsg
Normal file
Binary file not shown.
BIN
demo/demo-track-v3.smsg
Normal file
BIN
demo/demo-track-v3.smsg
Normal file
Binary file not shown.
3596
demo/index.html
Normal file
3596
demo/index.html
Normal file
File diff suppressed because it is too large
Load diff
BIN
demo/profile-avatar.jpg
Normal file
BIN
demo/profile-avatar.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
demo/stmf.wasm
Executable file
BIN
demo/stmf.wasm
Executable file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue