Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d90bef8580 | ||
|
|
6de3f1d714 |
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
+21
-2
@@ -10,5 +10,24 @@ modcache.tzst
|
|||||||
# Generated manifest files
|
# Generated manifest files
|
||||||
.index.mf
|
.index.mf
|
||||||
|
|
||||||
# Stale files
|
# Secrets
|
||||||
.drone.yml
|
.env
|
||||||
|
.env.*
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Editor files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Go build artifacts
|
||||||
|
*.log
|
||||||
|
*.out
|
||||||
|
*.test
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# mfer
|
# mfer
|
||||||
|
|
||||||
[mfer](https://git.eeqj.de/sneak/mfer) is a reference implementation library and
|
[mfer](https://git.eeqj.de/sneak/mfer) is a [WTFPL](https://wtfpl.net)-licensed
|
||||||
thin wrapper command-line utility written in [Go](https://golang.org) and first
|
(public domain) [Go](https://golang.org) library and command-line tool by
|
||||||
published in 2022 under the [WTFPL](https://wtfpl.net) (public domain) license.
|
[@sneak](https://sneak.berlin) that specifies and generates `.mf` manifest files
|
||||||
It specifies and generates `.mf` manifest files over a directory tree of files
|
over a directory tree to encapsulate metadata about the files — such as
|
||||||
to encapsulate metadata about them (such as cryptographic checksums or
|
cryptographic checksums and signatures over same — to aid in archiving,
|
||||||
signatures over same) to aid in archiving, downloading, and streaming, or
|
downloading, streaming, and mirroring. It was first published in 2022. The
|
||||||
mirroring. The manifest files' data is serialized with Google's
|
manifest files' data is serialized with Google's
|
||||||
[protobuf serialization format](https://developers.google.com/protocol-buffers).
|
[protobuf serialization format](https://developers.google.com/protocol-buffers).
|
||||||
The structure of these files can be found
|
The structure of these files can be found
|
||||||
[in the format specification](https://git.eeqj.de/sneak/mfer/src/branch/main/mfer/mf.proto)
|
[in the format specification](https://git.eeqj.de/sneak/mfer/src/branch/main/mfer/mf.proto)
|
||||||
@@ -21,6 +21,36 @@ This project was started by [@sneak](https://sneak.berlin) to scratch an itch in
|
|||||||
as a de-facto standard and be incorporated into other software. A compatible
|
as a de-facto standard and be incorporated into other software. A compatible
|
||||||
javascript library is planned.
|
javascript library is planned.
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
`mfer` builds from source with a Go 1.23+ toolchain. The generated protobuf code
|
||||||
|
is committed, so no `protoc` toolchain is required:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://git.eeqj.de/sneak/mfer.git
|
||||||
|
cd mfer
|
||||||
|
go build -o bin/mfer ./cmd/mfer
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate a manifest for a directory tree, verify it later, and fetch a published
|
||||||
|
tree by URL:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Write .index.mf describing every file under the current directory.
|
||||||
|
bin/mfer gen .
|
||||||
|
|
||||||
|
# Verify the files on disk against the manifest. Exits nonzero if any file
|
||||||
|
# is missing or corrupted.
|
||||||
|
bin/mfer check .index.mf
|
||||||
|
|
||||||
|
# Download and cryptographically verify a tree published over HTTP: mfer
|
||||||
|
# fetches <url>/index.mf, then downloads every file it lists.
|
||||||
|
bin/mfer fetch https://example.com/tree/
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `bin/mfer help` for the full command list, or `bin/mfer <command> --help`
|
||||||
|
for a single command's options.
|
||||||
|
|
||||||
# Build Status
|
# Build Status
|
||||||
|
|
||||||
CI runs via `script/cibuild` (`docker build .`), which executes `make check`
|
CI runs via `script/cibuild` (`docker build .`), which executes `make check`
|
||||||
@@ -81,7 +111,9 @@ Any changes submitted to this project must also be
|
|||||||
See [`REPO_POLICIES.md`](REPO_POLICIES.md) for detailed coding standards,
|
See [`REPO_POLICIES.md`](REPO_POLICIES.md) for detailed coding standards,
|
||||||
tooling requirements, and workflow conventions.
|
tooling requirements, and workflow conventions.
|
||||||
|
|
||||||
# Problem Statement
|
# Rationale
|
||||||
|
|
||||||
|
## The problem
|
||||||
|
|
||||||
Given a plain URL, there is no standard way to safely and programmatically
|
Given a plain URL, there is no standard way to safely and programmatically
|
||||||
download everything "under" that URL path. `wget -r` can traverse directory
|
download everything "under" that URL path. `wget -r` can traverse directory
|
||||||
@@ -105,7 +137,7 @@ Real issues I face:
|
|||||||
- when I download a large file via HTTP, I have no way of knowing if the file
|
- when I download a large file via HTTP, I have no way of knowing if the file
|
||||||
content is what it's supposed to be
|
content is what it's supposed to be
|
||||||
|
|
||||||
# Proposed Solution
|
## The solution
|
||||||
|
|
||||||
A standard, a manifest file format, and a tool for generating same.
|
A standard, a manifest file format, and a tool for generating same.
|
||||||
|
|
||||||
@@ -137,6 +169,27 @@ The manifest file would do several important things:
|
|||||||
- maybe a bittorrent chunklist for torrent client compatibility? perhaps a
|
- maybe a bittorrent chunklist for torrent client compatibility? perhaps a
|
||||||
top-level infohash for the whole manifest?
|
top-level infohash for the whole manifest?
|
||||||
|
|
||||||
|
# Design
|
||||||
|
|
||||||
|
The repository is split into a reusable library and a thin command-line wrapper
|
||||||
|
around it.
|
||||||
|
|
||||||
|
- `mfer/` is the reusable library and the heart of the project: it defines the
|
||||||
|
manifest format and implements building, scanning, checking, serialization,
|
||||||
|
and signing. The protobuf schema is `mfer/mf.proto`, and the generated code it
|
||||||
|
produces (`mfer/mf.pb.go`) is committed alongside it so the library builds
|
||||||
|
with `go get` and needs no `protoc` toolchain.
|
||||||
|
- `internal/cli/` holds the command implementations — `gen`, `check`, `freshen`,
|
||||||
|
`export`, `list`, and `fetch` — that wire the library to the command-line
|
||||||
|
interface.
|
||||||
|
- `internal/log/` provides the logging used across the commands.
|
||||||
|
- `internal/bork/` provides error-handling support.
|
||||||
|
- `cmd/mfer/` is the entrypoint: its `main` package assembles the pieces above
|
||||||
|
into the `mfer` binary.
|
||||||
|
|
||||||
|
Everything under `internal/` is private to this repository; only the `mfer/`
|
||||||
|
package is intended for import by other software.
|
||||||
|
|
||||||
# Design Goals
|
# Design Goals
|
||||||
|
|
||||||
- Replace SHASUMS/SHASUMS.asc files
|
- Replace SHASUMS/SHASUMS.asc files
|
||||||
@@ -462,9 +515,9 @@ proto `go_package` option. Which is canonical?
|
|||||||
- Issues:
|
- Issues:
|
||||||
[https://git.eeqj.de/sneak/mfer/issues](https://git.eeqj.de/sneak/mfer/issues)
|
[https://git.eeqj.de/sneak/mfer/issues](https://git.eeqj.de/sneak/mfer/issues)
|
||||||
|
|
||||||
# Authors
|
# Author
|
||||||
|
|
||||||
- [@sneak <sneak@sneak.berlin>](mailto:sneak@sneak.berlin)
|
- [@sneak](https://sneak.berlin)
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,14 @@ only thing left of the `chore/align-repo-policies` branch is the list below.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-09-21: fixed the `-v` collision between `--verbose` and `--version`;
|
- 2026-09-21: added the required README sections — named author, license, and
|
||||||
verbose owns `-v`, and version answers to `--version` and `-V` (#64)
|
category in the Description first line; added Getting Started (verified
|
||||||
|
install/usage block), Rationale (folding in Problem Statement and Proposed
|
||||||
|
Solution), and a Design section for the package layout; renamed Authors to
|
||||||
|
Author (#75)
|
||||||
|
- 2026-09-21: added the canonical `.editorconfig`, made `.gitignore` cover
|
||||||
|
secrets, OS, editor, and Go artifacts, and removed the dead Drone CI
|
||||||
|
references from `.gitignore` and `bin/gitrev.sh` (#72)
|
||||||
- 2026-08-09: added `.prettierrc`/`.prettierignore`, gave `script/fmt` and
|
- 2026-08-09: added `.prettierrc`/`.prettierignore`, gave `script/fmt` and
|
||||||
`script/fmt-check` one shared prettier file set via `script/prettier`, dropped
|
`script/fmt-check` one shared prettier file set via `script/prettier`, dropped
|
||||||
the `|| true` that hid prettier failures, and added a node-based Dockerfile
|
the `|| true` that hid prettier failures, and added a node-based Dockerfile
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
if [[ ! -z "$DRONE_COMMIT_SHA" ]]; then
|
|
||||||
echo "${DRONE_COMMIT_SHA:0:7}"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -z "$GITREV" ]]; then
|
if [[ ! -z "$GITREV" ]]; then
|
||||||
echo $GITREV
|
echo $GITREV
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ const (
|
|||||||
testManifest = "/manifest.mf"
|
testManifest = "/manifest.mf"
|
||||||
testFlagBase = "--base"
|
testFlagBase = "--base"
|
||||||
testFlagNoExtra = "--no-extra-files"
|
testFlagNoExtra = "--no-extra-files"
|
||||||
testFlagVersion = "--version"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var errSimulatedWrite = errors.New("simulated write failure")
|
var errSimulatedWrite = errors.New("simulated write failure")
|
||||||
@@ -103,7 +102,7 @@ func TestVersionCommand(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
opts := testOpts([]string{testApp, cmdVersion}, fs)
|
opts := testOpts([]string{testApp, "version"}, fs)
|
||||||
|
|
||||||
exitCode := runCLI(opts)
|
exitCode := runCLI(opts)
|
||||||
|
|
||||||
@@ -114,69 +113,6 @@ func TestVersionCommand(t *testing.T) {
|
|||||||
assert.Contains(t, stdout, "abc123")
|
assert.Contains(t, stdout, "abc123")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestVFlagCollision covers the -v/--verbose vs --version flag interaction
|
|
||||||
// (issue #64). Verbose owns -v; version answers to --version and -V. None of
|
|
||||||
// these invocations may produce a parser error, and the two ways of asking
|
|
||||||
// for the version must print the same thing.
|
|
||||||
func TestVFlagCollision(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Invocations that must print the version and exit 0.
|
|
||||||
versionCases := map[string][]string{
|
|
||||||
"long version flag": {testApp, testFlagVersion},
|
|
||||||
"short version flag": {testApp, "-V"},
|
|
||||||
"verbose then version": {testApp, "-v", testFlagVersion},
|
|
||||||
"long verbose and version": {testApp, "--verbose", testFlagVersion},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, args := range versionCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
opts := testOpts(args, afero.NewMemMapFs())
|
|
||||||
exitCode := runCLI(opts)
|
|
||||||
|
|
||||||
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
|
|
||||||
assert.Contains(t, testStdout(t, opts), mfer.Version)
|
|
||||||
assert.NotContains(t, testStderr(t, opts), "two forms of the same flag")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invocations that must enable verbose and exit 0 without a parser error.
|
|
||||||
verboseCases := map[string][]string{
|
|
||||||
"short verbose flag": {testApp, "-v"},
|
|
||||||
"long verbose flag": {testApp, "--verbose"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, args := range verboseCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
opts := testOpts(args, afero.NewMemMapFs())
|
|
||||||
exitCode := runCLI(opts)
|
|
||||||
|
|
||||||
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
|
|
||||||
assert.Contains(t, testStdout(t, opts), cmdGenerate,
|
|
||||||
"root should show help listing subcommands")
|
|
||||||
assert.Empty(t, testStderr(t, opts))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestVersionFlagAndCommandMatch asserts that "mfer --version" and
|
|
||||||
// "mfer version" produce identical output (issue #64).
|
|
||||||
func TestVersionFlagAndCommandMatch(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
flagOpts := testOpts([]string{testApp, testFlagVersion}, afero.NewMemMapFs())
|
|
||||||
require.Equal(t, 0, runCLI(flagOpts))
|
|
||||||
|
|
||||||
cmdOpts := testOpts([]string{testApp, cmdVersion}, afero.NewMemMapFs())
|
|
||||||
require.Equal(t, 0, runCLI(cmdOpts))
|
|
||||||
|
|
||||||
assert.Equal(t, testStdout(t, flagOpts), testStdout(t, cmdOpts))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHelpCommand(t *testing.T) {
|
func TestHelpCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
+2
-25
@@ -18,7 +18,6 @@ const (
|
|||||||
cmdGenerate = "generate"
|
cmdGenerate = "generate"
|
||||||
cmdCheck = "check"
|
cmdCheck = "check"
|
||||||
cmdExport = "export"
|
cmdExport = "export"
|
||||||
cmdVersion = "version"
|
|
||||||
|
|
||||||
flagProgress = "progress"
|
flagProgress = "progress"
|
||||||
|
|
||||||
@@ -68,12 +67,6 @@ func (mfa *CLIApp) VersionString() string {
|
|||||||
return mfer.Version
|
return mfer.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
// printVersion writes the version line shared by the --version flag and the
|
|
||||||
// version subcommand, so both produce identical output.
|
|
||||||
func (mfa *CLIApp) printVersion() {
|
|
||||||
_, _ = fmt.Fprintf(mfa.Stdout, "%s version %s\n", mfa.appname, mfa.VersionString())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mfa *CLIApp) printBanner() {
|
func (mfa *CLIApp) printBanner() {
|
||||||
if log.GetLevel() <= log.InfoLevel {
|
if log.GetLevel() <= log.InfoLevel {
|
||||||
_, _ = fmt.Fprintln(mfa.Stdout, banner)
|
_, _ = fmt.Fprintln(mfa.Stdout, banner)
|
||||||
@@ -272,10 +265,10 @@ func (mfa *CLIApp) exportCommand() *cli.Command {
|
|||||||
|
|
||||||
func (mfa *CLIApp) versionCommand() *cli.Command {
|
func (mfa *CLIApp) versionCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: cmdVersion,
|
Name: "version",
|
||||||
Usage: "Show version",
|
Usage: "Show version",
|
||||||
Action: func(_ *cli.Context) error {
|
Action: func(_ *cli.Context) error {
|
||||||
mfa.printVersion()
|
_, _ = fmt.Fprintln(mfa.Stdout, mfa.VersionString())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -331,20 +324,6 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
log.SetOutput(mfa.Stdout, mfa.Stderr)
|
log.SetOutput(mfa.Stdout, mfa.Stderr)
|
||||||
log.Init()
|
log.Init()
|
||||||
|
|
||||||
// -v means verbose, not version. urfave/cli's built-in version flag
|
|
||||||
// claims -v by default, colliding with the -v verbose alias used here and
|
|
||||||
// on every subcommand; that collision makes "mfer -v --version" fail to
|
|
||||||
// parse. Verbose is the more common meaning of -v in tools that offer
|
|
||||||
// both, so verbose keeps -v and the version flag takes the capital -V.
|
|
||||||
// VersionFlag and VersionPrinter are urfave/cli package globals; run() is
|
|
||||||
// serialized in tests, so assigning them here is safe.
|
|
||||||
cli.VersionFlag = &cli.BoolFlag{
|
|
||||||
Name: cmdVersion,
|
|
||||||
Aliases: []string{"V"},
|
|
||||||
Usage: "print the version",
|
|
||||||
}
|
|
||||||
cli.VersionPrinter = func(_ *cli.Context) { mfa.printVersion() }
|
|
||||||
|
|
||||||
mfa.app = &cli.App{
|
mfa.app = &cli.App{
|
||||||
Name: mfa.appname,
|
Name: mfa.appname,
|
||||||
Usage: "Manifest generator",
|
Usage: "Manifest generator",
|
||||||
@@ -352,13 +331,11 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
EnableBashCompletion: true,
|
EnableBashCompletion: true,
|
||||||
Writer: mfa.Stdout,
|
Writer: mfa.Stdout,
|
||||||
ErrWriter: mfa.Stderr,
|
ErrWriter: mfa.Stderr,
|
||||||
Flags: commonFlags(),
|
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
if c.Args().Len() > 0 {
|
if c.Args().Len() > 0 {
|
||||||
return fmt.Errorf("%w %q", errUnknownCommand, c.Args().First())
|
return fmt.Errorf("%w %q", errUnknownCommand, c.Args().First())
|
||||||
}
|
}
|
||||||
|
|
||||||
mfa.setVerbosity(c)
|
|
||||||
mfa.printBanner()
|
mfa.printBanner()
|
||||||
|
|
||||||
return cli.ShowAppHelp(c)
|
return cli.ShowAppHelp(c)
|
||||||
|
|||||||
Reference in New Issue
Block a user