[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}