[mirror] Command-line application for uploading a site to a git-pages server
1package main
2
3import (
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
18var passwordFlag = pflag.String("password", "", "password for DNS challenge authorization")
19var challengeFlag = pflag.Bool("challenge", false, "compute DNS challenge entry from password")
20var uploadGitFlag = pflag.String("upload-git", "", "replace site with contents of specified git repository")
21var uploadDirFlag = pflag.String("upload-dir", "", "replace site with contents of specified directory")
22var deleteFlag = pflag.Bool("delete", false, "delete site")
23var verboseFlag = pflag.Bool("verbose", false, "display more information for debugging")
24
25func 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
42func displayFS(root fs.FS) error {
43 return fs.WalkDir(root, ".", func(name string, entry fs.DirEntry, err error) error {
44 if err != nil {
45 return err
46 }
47 switch {
48 case entry.Type() == 0:
49 fmt.Fprintln(os.Stderr, "file", name)
50 case entry.Type() == fs.ModeDir:
51 fmt.Fprintln(os.Stderr, "dir", name)
52 case entry.Type() == fs.ModeSymlink:
53 fmt.Fprintln(os.Stderr, "symlink", name)
54 default:
55 fmt.Fprintln(os.Stderr, "other", name)
56 }
57 return nil
58 })
59}
60
61func archiveFS(root fs.FS) (result []byte, err error) {
62 buffer := bytes.Buffer{}
63 zstdWriter, _ := zstd.NewWriter(&buffer)
64 tarWriter := tar.NewWriter(zstdWriter)
65 err = tarWriter.AddFS(root)
66 if err != nil {
67 return
68 }
69 err = tarWriter.Close()
70 if err != nil {
71 return
72 }
73 err = zstdWriter.Close()
74 if err != nil {
75 return
76 }
77 result = buffer.Bytes()
78 return
79}
80
81func main() {
82 pflag.Parse()
83 if !singleOperation() || len(pflag.Args()) != 1 {
84 fmt.Fprintf(os.Stderr,
85 "Usage: %s <site-url> [--challenge|--upload-git url|--upload-dir path|--delete]\n",
86 os.Args[0],
87 )
88 os.Exit(125)
89 }
90
91 var err error
92 siteUrl, err := url.Parse(pflag.Args()[0])
93 if err != nil {
94 fmt.Fprintf(os.Stderr, "error: invalid site URL: %s\n", err)
95 os.Exit(1)
96 }
97
98 var request *http.Request
99 switch {
100 case *challengeFlag:
101 if *passwordFlag == "" {
102 fmt.Fprintf(os.Stderr, "error: no --password option specified\n")
103 os.Exit(1)
104 }
105
106 challenge := sha256.Sum256(fmt.Appendf(nil, "%s %s", siteUrl.Hostname(), *passwordFlag))
107 fmt.Fprintf(os.Stdout, "%s. 3600 IN TXT \"%x\"\n", siteUrl.Hostname(), challenge)
108 os.Exit(0)
109
110 case *uploadGitFlag != "":
111 uploadGitUrl, err := url.Parse(*uploadGitFlag)
112 if err != nil {
113 fmt.Fprintf(os.Stderr, "error: invalid repository URL: %s\n", err)
114 os.Exit(1)
115 }
116
117 requestBody := []byte(uploadGitUrl.String())
118 request, err = http.NewRequest("PUT", siteUrl.String(), bytes.NewReader(requestBody))
119 if err != nil {
120 fmt.Fprintf(os.Stderr, "error: %s\n", err)
121 os.Exit(1)
122 }
123 request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
124
125 case *uploadDirFlag != "":
126 uploadDirFS, err := os.OpenRoot(*uploadDirFlag)
127 if err != nil {
128 fmt.Fprintf(os.Stderr, "error: invalid directory: %s\n", err)
129 os.Exit(1)
130 }
131
132 if *verboseFlag {
133 err := displayFS(uploadDirFS.FS())
134 if err != nil {
135 fmt.Fprintf(os.Stderr, "error: %s\n", err)
136 os.Exit(1)
137 }
138 }
139
140 requestBody, err := archiveFS(uploadDirFS.FS())
141 if err != nil {
142 fmt.Fprintf(os.Stderr, "error: %s\n", err)
143 os.Exit(1)
144 }
145
146 request, err = http.NewRequest("PUT", siteUrl.String(), bytes.NewReader(requestBody))
147 if err != nil {
148 fmt.Fprintf(os.Stderr, "error: %s\n", err)
149 os.Exit(1)
150 }
151 request.Header.Add("Content-Type", "application/x-tar+zstd")
152
153 case *deleteFlag:
154 request, err = http.NewRequest("DELETE", siteUrl.String(), bytes.NewReader([]byte{}))
155 if err != nil {
156 fmt.Fprintf(os.Stderr, "error: %s\n", err)
157 os.Exit(1)
158 }
159
160 default:
161 panic("no operation chosen")
162 }
163 if *passwordFlag != "" {
164 request.Header.Add("Authorization", fmt.Sprintf("Pages %s", *passwordFlag))
165 }
166
167 response, err := http.DefaultClient.Do(request)
168 if err != nil {
169 fmt.Fprintf(os.Stderr, "error: %s\n", err)
170 os.Exit(1)
171 }
172 if *verboseFlag {
173 fmt.Fprintf(os.Stderr, "server: %s\n", response.Header.Get("Server"))
174 }
175 if response.StatusCode == 200 {
176 fmt.Fprintf(os.Stdout, "result: %s\n", response.Header.Get("Update-Result"))
177 os.Exit(0)
178 } else {
179 fmt.Fprintf(os.Stderr, "result: error\n")
180 io.Copy(os.Stderr, response.Body)
181 os.Exit(1)
182 }
183}