[mirror] Command-line application for uploading a site to a git-pages server
at v1.0.0 6.1 kB view raw
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/google/uuid" 15 "github.com/klauspost/compress/zstd" 16 "github.com/spf13/pflag" 17) 18 19var passwordFlag = pflag.String("password", "", "password for DNS challenge authorization") 20var challengeFlag = pflag.Bool("challenge", false, "compute DNS challenge entry from password (output zone file record)") 21var challengeBareFlag = pflag.Bool("challenge-bare", false, "compute DNS challenge entry from password (output bare TXT value)") 22var uploadGitFlag = pflag.String("upload-git", "", "replace site with contents of specified git repository") 23var uploadDirFlag = pflag.String("upload-dir", "", "replace site with contents of specified directory") 24var deleteFlag = pflag.Bool("delete", false, "delete site") 25var debugManifestFlag = pflag.Bool("debug-manifest", false, "retrieve site manifest as ProtoJSON, for debugging") 26var serverFlag = pflag.String("server", "", "hostname of server to connect to") 27var verboseFlag = pflag.Bool("verbose", false, "display more information for debugging") 28 29func singleOperation() bool { 30 operations := 0 31 if *challengeFlag { 32 operations++ 33 } 34 if *challengeBareFlag { 35 operations++ 36 } 37 if *uploadDirFlag != "" { 38 operations++ 39 } 40 if *uploadGitFlag != "" { 41 operations++ 42 } 43 if *deleteFlag { 44 operations++ 45 } 46 if *debugManifestFlag { 47 operations++ 48 } 49 return operations == 1 50} 51 52func displayFS(root fs.FS) error { 53 return fs.WalkDir(root, ".", func(name string, entry fs.DirEntry, err error) error { 54 if err != nil { 55 return err 56 } 57 switch { 58 case entry.Type() == 0: 59 fmt.Fprintln(os.Stderr, "file", name) 60 case entry.Type() == fs.ModeDir: 61 fmt.Fprintln(os.Stderr, "dir", name) 62 case entry.Type() == fs.ModeSymlink: 63 fmt.Fprintln(os.Stderr, "symlink", name) 64 default: 65 fmt.Fprintln(os.Stderr, "other", name) 66 } 67 return nil 68 }) 69} 70 71func archiveFS(root fs.FS) (result []byte, err error) { 72 buffer := bytes.Buffer{} 73 zstdWriter, _ := zstd.NewWriter(&buffer) 74 tarWriter := tar.NewWriter(zstdWriter) 75 err = tarWriter.AddFS(root) 76 if err != nil { 77 return 78 } 79 err = tarWriter.Close() 80 if err != nil { 81 return 82 } 83 err = zstdWriter.Close() 84 if err != nil { 85 return 86 } 87 result = buffer.Bytes() 88 return 89} 90 91func main() { 92 pflag.Parse() 93 if !singleOperation() || len(pflag.Args()) != 1 { 94 fmt.Fprintf(os.Stderr, 95 "Usage: %s <site-url> [--challenge|--upload-git url|--upload-dir path|--delete]\n", 96 os.Args[0], 97 ) 98 os.Exit(125) 99 } 100 101 var err error 102 siteURL, err := url.Parse(pflag.Args()[0]) 103 if err != nil { 104 fmt.Fprintf(os.Stderr, "error: invalid site URL: %s\n", err) 105 os.Exit(1) 106 } 107 108 var request *http.Request 109 switch { 110 case *challengeFlag || *challengeBareFlag: 111 if *passwordFlag == "" { 112 *passwordFlag = uuid.NewString() 113 fmt.Fprintf(os.Stderr, "password: %s\n", *passwordFlag) 114 } 115 116 challenge := sha256.Sum256(fmt.Appendf(nil, "%s %s", siteURL.Hostname(), *passwordFlag)) 117 if *challengeBareFlag { 118 fmt.Fprintf(os.Stdout, "%x\n", challenge) 119 } else { 120 fmt.Fprintf(os.Stdout, "_git-pages-challenge.%s. 3600 IN TXT \"%x\"\n", siteURL.Hostname(), challenge) 121 } 122 os.Exit(0) 123 124 case *uploadGitFlag != "": 125 uploadGitUrl, err := url.Parse(*uploadGitFlag) 126 if err != nil { 127 fmt.Fprintf(os.Stderr, "error: invalid repository URL: %s\n", err) 128 os.Exit(1) 129 } 130 131 requestBody := []byte(uploadGitUrl.String()) 132 request, err = http.NewRequest("PUT", siteURL.String(), bytes.NewReader(requestBody)) 133 if err != nil { 134 fmt.Fprintf(os.Stderr, "error: %s\n", err) 135 os.Exit(1) 136 } 137 request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 138 139 case *uploadDirFlag != "": 140 uploadDirFS, err := os.OpenRoot(*uploadDirFlag) 141 if err != nil { 142 fmt.Fprintf(os.Stderr, "error: invalid directory: %s\n", err) 143 os.Exit(1) 144 } 145 146 if *verboseFlag { 147 err := displayFS(uploadDirFS.FS()) 148 if err != nil { 149 fmt.Fprintf(os.Stderr, "error: %s\n", err) 150 os.Exit(1) 151 } 152 } 153 154 requestBody, err := archiveFS(uploadDirFS.FS()) 155 if err != nil { 156 fmt.Fprintf(os.Stderr, "error: %s\n", err) 157 os.Exit(1) 158 } 159 160 request, err = http.NewRequest("PUT", siteURL.String(), bytes.NewReader(requestBody)) 161 if err != nil { 162 fmt.Fprintf(os.Stderr, "error: %s\n", err) 163 os.Exit(1) 164 } 165 request.Header.Add("Content-Type", "application/x-tar+zstd") 166 167 case *deleteFlag: 168 request, err = http.NewRequest("DELETE", siteURL.String(), bytes.NewReader([]byte{})) 169 if err != nil { 170 fmt.Fprintf(os.Stderr, "error: %s\n", err) 171 os.Exit(1) 172 } 173 174 case *debugManifestFlag: 175 manifestURL := siteURL.ResolveReference(&url.URL{Path: ".git-pages/manifest.json"}) 176 request, err = http.NewRequest("GET", manifestURL.String(), bytes.NewReader([]byte{})) 177 if err != nil { 178 fmt.Fprintf(os.Stderr, "error: %s\n", err) 179 os.Exit(1) 180 } 181 182 default: 183 panic("no operation chosen") 184 } 185 if *passwordFlag != "" { 186 request.Header.Add("Authorization", fmt.Sprintf("Pages %s", *passwordFlag)) 187 } 188 if *serverFlag != "" { 189 // Send the request to `--server` host, but set the `Host:` header to the site host. 190 // This allows first-time publishing to proceed without the git-pages server yet having 191 // a TLS certificate for the site host (which has a circular dependency on completion of 192 // first-time publishing). 193 newURL := *request.URL 194 newURL.Host = *serverFlag 195 request.URL = &newURL 196 request.Header.Set("Host", siteURL.Host) 197 } 198 199 response, err := http.DefaultClient.Do(request) 200 if err != nil { 201 fmt.Fprintf(os.Stderr, "error: %s\n", err) 202 os.Exit(1) 203 } 204 if *verboseFlag { 205 fmt.Fprintf(os.Stderr, "server: %s\n", response.Header.Get("Server")) 206 } 207 if *debugManifestFlag { 208 if response.StatusCode == 200 { 209 io.Copy(os.Stdout, response.Body) 210 fmt.Fprintf(os.Stdout, "\n") 211 } else { 212 io.Copy(os.Stderr, response.Body) 213 os.Exit(1) 214 } 215 } else { // an update operation 216 if response.StatusCode == 200 { 217 fmt.Fprintf(os.Stdout, "result: %s\n", response.Header.Get("Update-Result")) 218 io.Copy(os.Stdout, response.Body) 219 } else { 220 fmt.Fprintf(os.Stderr, "result: error\n") 221 io.Copy(os.Stderr, response.Body) 222 os.Exit(1) 223 } 224 } 225}