diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3df13a..0c86dc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,10 +11,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go_version: [1.18, 1.19] + go_version: [ 1.18, 1.19 ] steps: - name: Checkout uses: actions/checkout@v3 + - name: Install Dependencies + run: | + sudo apt update + sudo apt install -y webp - name: Setup Go uses: actions/setup-go@v3 with: diff --git a/Dockerfile b/Dockerfile index e3d98fb..ac17290 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,10 +6,15 @@ WORKDIR /go/src/oyaki COPY . /go/src/oyaki RUN CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${OYAKI_VERSION}" -o /go/bin/oyaki +RUN apt update && apt install -y curl \ + && curl https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.3.1-linux-x86-64.tar.gz --output libwebp.tar.gz \ + && tar vzxf libwebp.tar.gz \ + && mv libwebp-1.3.1-linux-x86-64/bin/cwebp /go/bin/ FROM gcr.io/distroless/static-debian11 COPY --from=build /go/bin/oyaki / +COPY --from=build /go/bin/cwebp /bin/ EXPOSE 8080 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/main.go b/main.go index db685a5..b8929ea 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "os" + "path/filepath" "runtime/debug" "strconv" "syscall" @@ -77,12 +78,22 @@ func proxy(w http.ResponseWriter, r *http.Request) { if len(xff) > 1 { req.Header.Set("X-Forwarded-For", xff) } - - orgRes, err := client.Do(req) - if err != nil { - http.Error(w, "Get origin failed", http.StatusBadGateway) - log.Printf("Get origin failed. %v\n", err) - return + var orgRes *http.Response + pathExt := filepath.Ext(req.URL.Path) + if pathExt == ".webp" { + orgRes, err = doWebp(req) + if err != nil { + http.Error(w, "Get origin failed", http.StatusBadGateway) + log.Printf("Get origin failed. %v\n", err) + return + } + } else { + orgRes, err = client.Do(req) + if err != nil { + http.Error(w, "Get origin failed", http.StatusBadGateway) + log.Printf("Get origin failed. %v\n", err) + return + } } defer orgRes.Body.Close() @@ -130,8 +141,18 @@ func proxy(w http.ResponseWriter, r *http.Request) { return } defer buf.Reset() + if pathExt == ".webp" { + buf, err = convWebp(buf, []string{}) + if err != nil { + http.Error(w, "image convert failed", http.StatusInternalServerError) + log.Printf("Read origin body failed. %v\n", err) + return - w.Header().Set("Content-Type", "image/jpeg") + } + w.Header().Set("Content-Type", "image/webp") + } else { + w.Header().Set("Content-Type", "image/jpeg") + } w.Header().Set("Content-Length", strconv.Itoa(buf.Len())) if _, err := io.Copy(w, buf); err != nil { diff --git a/webp.go b/webp.go new file mode 100644 index 0000000..9ae0a7c --- /dev/null +++ b/webp.go @@ -0,0 +1,60 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/exec" +) + +func doWebp(req *http.Request) (*http.Response, error) { + var orgRes *http.Response + orgURL := req.URL + newPath := orgURL.Path[:len(orgURL.Path)-len(".webp")] + newOrgURL, err := url.Parse(fmt.Sprintf("%s://%s%s?%s", orgURL.Scheme, orgURL.Host, newPath, orgURL.RawQuery)) + if err != nil { + log.Println(err) + return nil, err + } + newReq, err := http.NewRequest("GET", newOrgURL.String(), nil) + newReq.Header = req.Header + if err != nil { + log.Println(err) + return nil, err + } + orgRes, err = client.Do(newReq) + if err != nil { + log.Println(err) + return nil, err + } + if orgRes.StatusCode != 200 { + log.Println(orgRes.Status) + return nil, err + } + return orgRes, nil +} + +func convWebp(src io.Reader, params []string) (*bytes.Buffer, error) { + f, err := os.CreateTemp("/tmp", "") + if err != nil { + return nil, err + } + defer f.Close() + defer os.Remove(f.Name()) + + _, err = io.Copy(f, src) + if err != nil { + return nil, err + } + params = append(params, "-quiet", "-mt", "-jpeg_like", f.Name(), "-o", "-") + out, err := exec.Command("cwebp", params...).Output() + if err != nil { + log.Println(err) + return nil, err + } + return bytes.NewBuffer(out), nil +} diff --git a/webp_test.go b/webp_test.go new file mode 100644 index 0000000..c67dd62 --- /dev/null +++ b/webp_test.go @@ -0,0 +1,56 @@ +package main + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func TestProxyWebP(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(proxy)) + + origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./testdata/oyaki.jpg") + })) + + orgSrvURL = origin.URL + url := ts.URL + "/oyaki.jpg.webp" + + req, _ := http.NewRequest("GET", url, nil) + resp, err := doWebp(req) + if err != nil { + t.Fatal(err) + } else { + io.ReadAll(resp.Body) + resp.Body.Close() + } + // match with origin file info + if resp.Header.Get("Content-Type") != "image/jpeg" { + t.Error("wrong header Content-Type") + t.Error(resp.Header) + } +} + +func TestConvJPG2WebP(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(proxy)) + + origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./testdata/oyaki.jpg") + })) + + orgSrvURL = origin.URL + url := ts.URL + "/oyaki.jpg.webp" + + req, _ := http.NewRequest("GET", url, nil) + resp, err := doWebp(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + _, err = convWebp(resp.Body, []string{}) + if err != nil { + t.Fatal(err) + } + +}