+17
-2
README.md
+17
-2
README.md
···
1
git-pages-cli
2
=============
3
4
+
_git-pages-cli_ is a command-line application for uploading sites to [git-pages].
5
6
[git-pages]: https://codeberg.org/git-pages/git-pages
7
···
19
Usage
20
-----
21
22
+
To deploy a site from a git repository available on the internet (`--password` may be omitted if the repository is allowlisted via DNS):
23
+
24
+
```console
25
+
$ git-pages-cli https://mycoolweb.site --password xyz --upload-git https://codeberg.org/username/mycoolweb.site.git
26
+
```
27
+
28
+
To deploy a site from a directory on your machine:
29
+
30
+
```console
31
+
$ git-pages-cli https://mycoolweb.site --password xyz --upload-dir site-contents
32
+
```
33
+
34
+
To delete a site:
35
36
+
```console
37
+
$ git-pages-cli https://mycoolweb.site --password xyz --delete
38
+
```
39
40
41
License
+5
go.mod
+5
go.mod
+4
go.sum
+4
go.sum
···
···
1
+
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
2
+
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
3
+
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
4
+
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+149
main.go
+149
main.go
···
1
package main
2
3
+
import (
4
+
"archive/tar"
5
+
"bytes"
6
+
"crypto/sha256"
7
+
"fmt"
8
+
"io"
9
+
"io/fs"
10
+
"net/http"
11
+
"net/url"
12
+
"os"
13
+
14
+
"github.com/klauspost/compress/zstd"
15
+
"github.com/spf13/pflag"
16
+
)
17
+
18
+
var passwordFlag = pflag.String("password", "", "password for DNS challenge authorization")
19
+
var challengeFlag = pflag.Bool("challenge", false, "compute DNS challenge entry from password")
20
+
var uploadGitFlag = pflag.String("upload-git", "", "replace site with contents of specified git repository")
21
+
var uploadDirFlag = pflag.String("upload-dir", "", "replace site with contents of specified directory")
22
+
var deleteFlag = pflag.Bool("delete", false, "delete site")
23
+
var verboseFlag = pflag.Bool("verbose", false, "display more information for debugging")
24
+
25
+
func singleOperation() bool {
26
+
operations := 0
27
+
if *challengeFlag {
28
+
operations++
29
+
}
30
+
if *uploadDirFlag != "" {
31
+
operations++
32
+
}
33
+
if *uploadGitFlag != "" {
34
+
operations++
35
+
}
36
+
if *deleteFlag {
37
+
operations++
38
+
}
39
+
return operations == 1
40
+
}
41
+
42
+
func archiveFS(fs fs.FS) (result []byte, err error) {
43
+
buffer := bytes.Buffer{}
44
+
zstdWriter, _ := zstd.NewWriter(&buffer)
45
+
tarWriter := tar.NewWriter(zstdWriter)
46
+
err = tarWriter.AddFS(fs)
47
+
if err != nil {
48
+
return
49
+
}
50
+
err = tarWriter.Close()
51
+
if err != nil {
52
+
return
53
+
}
54
+
err = zstdWriter.Close()
55
+
if err != nil {
56
+
return
57
+
}
58
+
result = buffer.Bytes()
59
+
return
60
+
}
61
+
62
func main() {
63
+
pflag.Parse()
64
+
if singleOperation() || len(pflag.Args()) != 1 {
65
+
fmt.Fprintf(os.Stderr, "Usage: %s [-upload-git url|-upload-dir path|-delete] <site-url>\n", os.Args[0])
66
+
os.Exit(125)
67
+
}
68
+
69
+
var err error
70
+
siteUrl, err := url.Parse(pflag.Args()[0])
71
+
if err != nil {
72
+
fmt.Fprintf(os.Stderr, "error: invalid site URL: %s\n", err)
73
+
os.Exit(1)
74
+
}
75
+
76
+
var request *http.Request
77
+
switch {
78
+
case *challengeFlag:
79
+
if *passwordFlag == "" {
80
+
fmt.Fprintf(os.Stderr, "error: no password specified")
81
+
os.Exit(1)
82
+
}
83
+
84
+
challenge := sha256.Sum256(fmt.Appendf(nil, "%s %s", siteUrl.Hostname(), *passwordFlag))
85
+
fmt.Fprintf(os.Stdout, "%s. 3600 IN TXT \"%x\"\n", siteUrl.Hostname(), challenge)
86
+
os.Exit(0)
87
+
88
+
case *uploadGitFlag != "":
89
+
uploadGitUrl, err := url.Parse(*uploadGitFlag)
90
+
if err != nil {
91
+
fmt.Fprintf(os.Stderr, "error: invalid repository URL: %s\n", err)
92
+
os.Exit(1)
93
+
}
94
+
95
+
requestBody := []byte(uploadGitUrl.String())
96
+
request, err = http.NewRequest("PUT", siteUrl.String(), bytes.NewReader(requestBody))
97
+
if err != nil {
98
+
fmt.Fprintf(os.Stderr, "error: %s\n", err)
99
+
os.Exit(1)
100
+
}
101
+
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
102
+
103
+
case *uploadDirFlag != "":
104
+
uploadDirFS, err := os.OpenRoot(*uploadDirFlag)
105
+
if err != nil {
106
+
fmt.Fprintf(os.Stderr, "error: invalid directory: %s\n", err)
107
+
os.Exit(1)
108
+
}
109
+
110
+
requestBody, err := archiveFS(uploadDirFS.FS())
111
+
if err != nil {
112
+
fmt.Fprintf(os.Stderr, "error: %s\n", err)
113
+
os.Exit(1)
114
+
}
115
+
116
+
request, err = http.NewRequest("PUT", siteUrl.String(), bytes.NewReader(requestBody))
117
+
if err != nil {
118
+
fmt.Fprintf(os.Stderr, "error: %s\n", err)
119
+
os.Exit(1)
120
+
}
121
+
request.Header.Add("Content-Type", "application/x-tar+zstd")
122
+
123
+
case *deleteFlag:
124
+
request, err = http.NewRequest("DELETE", siteUrl.String(), bytes.NewReader([]byte{}))
125
+
if err != nil {
126
+
fmt.Fprintf(os.Stderr, "error: %s\n", err)
127
+
os.Exit(1)
128
+
}
129
+
130
+
default:
131
+
panic("no operation chosen")
132
+
}
133
+
if *passwordFlag != "" {
134
+
request.Header.Add("Authorization", fmt.Sprintf("Pages %s", *passwordFlag))
135
+
}
136
+
137
+
response, err := http.DefaultClient.Do(request)
138
+
if err != nil {
139
+
fmt.Fprintf(os.Stderr, "error: %s\n", err)
140
+
os.Exit(1)
141
+
}
142
+
if *verboseFlag {
143
+
fmt.Fprintf(os.Stderr, "server: %s\n", response.Header.Get("Server"))
144
+
}
145
+
if response.StatusCode == 200 {
146
+
fmt.Fprintf(os.Stdout, "result: %s\n", response.Header.Get("Update-Result"))
147
+
os.Exit(0)
148
+
} else {
149
+
fmt.Fprintf(os.Stderr, "result: error\n")
150
+
io.Copy(os.Stderr, response.Body)
151
+
os.Exit(1)
152
+
}
153
}