diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1f06419 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,62 @@ +name: 发行版 +on: + push: + tags: + - v* + +env: + GITHUB_TOKEN: ${{ github.token }} + +jobs: + my-job: + name: Build imoto on Push Tag 🚀 + runs-on: ubuntu-latest + steps: + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: '1.21' + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Cache Go + id: cache + uses: actions/cache@v2 + with: + # A list of files, directories, and wildcard patterns to cache and restore + path: ~/go/pkg/mod + key: ${{ runner.os }}-build-${{ hashFiles('**/go.sum') }} + + - name: Tidy Go modules + run: go mod tidy + + - name: Build linux-x64 + run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o artifacts/imoto-linux-x64 + - name: Build linux-x86 + run: CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags="-s -w" -trimpath -o artifacts/imoto-linux-x86 + + - name: Build windows-x64 + run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o artifacts/imoto-windows-x64.exe + - name: Build windows-x86 + run: CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags="-s -w" -trimpath -o artifacts/imoto-windows-x86.exe + + - name: Build linux-arm64 + run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build -ldflags="-s -w" -trimpath -o artifacts/imoto-linux-arm64 + - name: Build linux-armhfv6 + run: CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-s -w" -trimpath -o artifacts/imoto-linux-armhfv6 + + - name: Build darwin-x64 + run: CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o artifacts/imoto-darwin-x64 + - name: Build darwin-arm64 + run: CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -trimpath -o artifacts/imoto-darwin-arm64 + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: artifacts/imoto-* + tag: ${{ github.ref }} + overwrite: true + file_glob: true diff --git a/README.md b/README.md new file mode 100644 index 0000000..82db06d --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# imoto +图片限时缓存图床 + +# 使用 +详见文件夹`test` diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..e7287a5 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "bytes" + "crypto/md5" + "flag" + "io" + "net/http" + "time" + + "github.com/FloatTech/ttl" + "github.com/fumiama/imgsz" + "github.com/sirupsen/logrus" + + "github.com/fumiama/imoto" +) + +var imgcache *ttl.Cache[uint64, *imagebody] + +type imagebody struct { + key uint64 + typ string + dat []byte +} + +func main() { + cachetime := flag.Uint("t", 60, "cache time (s)") + endpoint := flag.String("e", "127.0.0.1:8000", "listening endpoint") + flag.Parse() + imgcache = ttl.NewCache[uint64, *imagebody](time.Second * time.Duration(*cachetime)) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + m, err := imoto.GetMD5(r.URL.Path) + defer r.Body.Close() + if err != nil { + http.Error(w, "400 Bad Request: "+err.Error(), http.StatusBadRequest) + return + } + p, k := imoto.SplitMD5(m) + logrus.Infoln("[handle]", r.Method, r.URL.Path) + switch r.Method { + case http.MethodHead: + if imgcache.Get(p) == nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusOK) + case http.MethodGet: + img := imgcache.Get(p) + if img == nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "image/"+img.typ) + _, _ = w.Write(img.dat) + case http.MethodPut: + data, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "500 Internal Server Error: "+err.Error(), http.StatusInternalServerError) + return + } + realm := md5.Sum(data) + if m != realm { + http.Error(w, "400 Bad Request: file md5 mismatch", http.StatusBadRequest) + return + } + _, typ, err := imgsz.DecodeSize(bytes.NewReader(data)) + if err != nil { + http.Error(w, "400 Bad Request: "+err.Error(), http.StatusBadRequest) + return + } + imgcache.Set(p, &imagebody{ + key: k, + typ: typ, + dat: data, + }) + w.WriteHeader(http.StatusOK) + case http.MethodDelete: + img := imgcache.Get(p) + if img == nil { + w.WriteHeader(http.StatusNotFound) + return + } + if k != img.key { + http.Error(w, "403 Forbidden", http.StatusForbidden) + return + } + imgcache.Delete(p) + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "image/"+img.typ) + _, _ = w.Write(img.dat) + default: + http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed) + } + }) + logrus.Errorln(http.ListenAndServe(*endpoint, nil)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..629d58b --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/fumiama/imoto + +go 1.20 + +require ( + github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1 + github.com/fumiama/imgsz v0.0.2 + github.com/sirupsen/logrus v1.9.3 +) + +require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c8a639b --- /dev/null +++ b/go.sum @@ -0,0 +1,19 @@ +github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1 h1:g4pTnDJUW4VbJ9NvoRfUvdjDrHz/6QhfN/LoIIpICbo= +github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fumiama/imgsz v0.0.2 h1:fAkC0FnIscdKOXwAxlyw3EUba5NzxZdSxGaq3Uyfxak= +github.com/fumiama/imgsz v0.0.2/go.mod h1:dR71mI3I2O5u6+PCpd47M9TZptzP+39tRBcbdIkoqM4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..ff5cf78 --- /dev/null +++ b/helper.go @@ -0,0 +1,60 @@ +package imoto + +import ( + "crypto/md5" + "encoding/binary" + "encoding/hex" + "errors" + "strconv" + "strings" + "unsafe" +) + +func GetMD5(u string) (m [md5.Size]byte, err error) { + u = strings.Trim(u, "/ ?&\n\t") + if len(u) != md5.Size*2 && len(u) != md5.Size { + err = errors.New("invalid path len: " + strconv.Itoa(len(u))) + return + } + _, err = hex.Decode(m[:], StringToBytes(u)) + return +} + +func SplitMD5(m [md5.Size]byte) (path uint64, key uint64) { + path = binary.LittleEndian.Uint64(m[:8]) + key = binary.LittleEndian.Uint64(m[8:]) + return +} + +func Uint64String(x uint64) string { + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], x) + return hex.EncodeToString(buf[:]) +} + +// slice is the runtime representation of a slice. +// It cannot be used safely or portably and its representation may +// change in a later release. +// +// Unlike reflect.SliceHeader, its Data field is sufficient to guarantee the +// data it references will not be garbage collected. +type slice struct { + data unsafe.Pointer + len int + cap int +} + +// BytesToString 没有内存开销的转换 +func BytesToString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} + +// StringToBytes 没有内存开销的转换 +func StringToBytes(s string) (b []byte) { + bh := (*slice)(unsafe.Pointer(&b)) + sh := (*slice)(unsafe.Pointer(&s)) + bh.data = sh.data + bh.len = sh.len + bh.cap = sh.len + return b +} diff --git a/test/main.go b/test/main.go new file mode 100644 index 0000000..e0d4f2f --- /dev/null +++ b/test/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "io" + "net/http" + "os" + + "github.com/fumiama/imoto" +) + +func main() { + data, err := os.ReadFile("test.jpeg") + if err != nil { + panic(err) + } + m := md5.Sum(data) + p, _ := imoto.SplitMD5(m) + req, err := http.NewRequest("PUT", "http://127.0.0.1:8000/"+hex.EncodeToString(m[:]), bytes.NewReader(data)) + if err != nil { + panic(err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + msg, _ := io.ReadAll(resp.Body) + panic("PUT error: " + imoto.BytesToString(msg)) + } + req, err = http.NewRequest("HEAD", "http://127.0.0.1:8000/"+imoto.Uint64String(p), nil) + if err != nil { + panic(err) + } + resp, err = http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + if resp.StatusCode != http.StatusOK { + msg, _ := io.ReadAll(resp.Body) + panic("HEAD error: " + imoto.BytesToString(msg)) + } + req, err = http.NewRequest("GET", "http://127.0.0.1:8000/"+imoto.Uint64String(p), nil) + if err != nil { + panic(err) + } + resp, err = http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + msg, _ := io.ReadAll(resp.Body) + panic("HEAD error: " + imoto.BytesToString(msg)) + } + h := md5.New() + _, err = io.Copy(h, resp.Body) + if err != nil { + panic(err) + } + var m2 [md5.Size]byte + h.Sum(m2[:0]) + if m2 != m { + panic("GET error: expected " + hex.EncodeToString(m[:]) + " but got " + hex.EncodeToString(m2[:])) + } + req, err = http.NewRequest("DELETE", "http://127.0.0.1:8000/"+hex.EncodeToString(m[:]), nil) + if err != nil { + panic(err) + } + resp, err = http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + msg, _ := io.ReadAll(resp.Body) + panic("HEAD error: " + imoto.BytesToString(msg)) + } + h = md5.New() + _, err = io.Copy(h, resp.Body) + if err != nil { + panic(err) + } + h.Sum(m2[:0]) + if m2 != m { + panic("DELETE error: expected " + hex.EncodeToString(m[:]) + " but got " + hex.EncodeToString(m2[:])) + } +} diff --git a/test/test.jpeg b/test/test.jpeg new file mode 100644 index 0000000..69ae155 Binary files /dev/null and b/test/test.jpeg differ