1
0
mirror of https://github.com/fumiama/imoto.git synced 2026-06-27 07:20:24 +08:00
This commit is contained in:
源文雨
2023-11-17 01:46:29 +09:00
parent 5221a81e6f
commit afcdf158c3
8 changed files with 345 additions and 0 deletions

62
.github/workflows/release.yml vendored Normal file
View File

@@ -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

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# imoto
图片限时缓存图床
# 使用
详见文件夹`test`

97
cmd/main.go Normal file
View File

@@ -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))
}

11
go.mod Normal file
View File

@@ -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

19
go.sum Normal file
View File

@@ -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=

60
helper.go Normal file
View File

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

91
test/main.go Normal file
View File

@@ -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[:]))
}
}

BIN
test/test.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB