Skip to content

Commit

Permalink
assets: add file system layer for zipped embed assets
Browse files Browse the repository at this point in the history
This commits adds an optional decompression layer to a embed.FS
instance. If a file can't be found during Open, the filename plus a .gz
ending is tried. If this succeeds, the content is decompressed and
served.

Signed-off-by: Jan Fajerski <[email protected]>
  • Loading branch information
jan--f authored and roidelapluie committed Nov 30, 2021
1 parent ce7006e commit 1871a70
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This repository contains Go libraries that are shared across Prometheus
components and libraries. They are considered internal to Prometheus, without
any stability guarantees for external usage.

* **assets**: Embedding of static assets with gzip support
* **config**: Common configuration structures
* **expfmt**: Decoding and encoding for the exposition format
* **model**: Shared data structures
Expand Down
18 changes: 18 additions & 0 deletions assets/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2018 The Prometheus Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

include ../Makefile.common

.PHONY: test
@echo ">> Running assets tests"
test:: deps check_license unused common-test
123 changes: 123 additions & 0 deletions assets/embed_gzip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2021 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package assets

import (
"compress/gzip"
"embed"
"io"
"io/fs"
"time"
)

const (
gzipSuffix = ".gz"
)

type FileSystem struct {
embed embed.FS
}

func New(fs embed.FS) FileSystem {
return FileSystem{fs}
}

// Open implements the fs.FS interface.
func (compressed FileSystem) Open(path string) (fs.File, error) {
// If we have the file in our embed FS, just return that as it could be a dir.
var f fs.File
if f, err := compressed.embed.Open(path); err == nil {
return f, nil
}

f, err := compressed.embed.Open(path + gzipSuffix)
if err != nil {
return f, err
}
// Read the decompressed content into a buffer.
gr, err := gzip.NewReader(f)
if err != nil {
return f, err
}
defer gr.Close()

c, err := io.ReadAll(gr)
if err != nil {
return f, err
}
// Wrap everything in our custom File.
return &File{file: f, content: c}, nil
}

type File struct {
// The underlying file.
file fs.File
// The decrompressed content, needed to return an accurate size.
content []byte
// Offset for calls to Read().
offset int
}

// Stat implements the fs.File interface.
func (f File) Stat() (fs.FileInfo, error) {
stat, err := f.file.Stat()
if err != nil {
return stat, err
}
return FileInfo{stat, int64(len(f.content))}, nil
}

// Read implements the fs.File interface.
func (f *File) Read(buf []byte) (int, error) {
if len(buf) > len(f.content)-f.offset {
buf = buf[0:len(f.content[f.offset:])]
}
n := copy(buf, f.content[f.offset:])
if n == len(f.content)-f.offset {
return n, io.EOF
}
f.offset += n
return n, nil
}

// Close implements the fs.File interface.
func (f File) Close() error {
return f.file.Close()
}

type FileInfo struct {
fi fs.FileInfo
actualSize int64
}

// Name implements the fs.FileInfo interface.
func (fi FileInfo) Name() string {
name := fi.fi.Name()
return name[:len(name)-len(gzipSuffix)]
}

// Size implements the fs.FileInfo interface.
func (fi FileInfo) Size() int64 { return fi.actualSize }

// Mode implements the fs.FileInfo interface.
func (fi FileInfo) Mode() fs.FileMode { return fi.fi.Mode() }

// ModTime implements the fs.FileInfo interface.
func (fi FileInfo) ModTime() time.Time { return fi.fi.ModTime() }

// IsDir implements the fs.FileInfo interface.
func (fi FileInfo) IsDir() bool { return fi.fi.IsDir() }

// Sys implements the fs.FileInfo interface.
func (fi FileInfo) Sys() interface{} { return nil }
93 changes: 93 additions & 0 deletions assets/embed_gzip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2021 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package assets

import (
"embed"
"io/ioutil"
"strings"
"testing"
)

//go:embed testdata
var EmbedFS embed.FS

var testFS = New(EmbedFS)

func TestFS(t *testing.T) {
cases := []struct {
name string
path string
expectedSize int64
expectedContent string
}{
{
name: "uncompressed file",
path: "testdata/uncompressed",
expectedSize: 4,
expectedContent: "foo\n",
},
{
name: "compressed file",
path: "testdata/compressed",
expectedSize: 4,
expectedContent: "foo\n",
},
{
name: "both, open uncompressed",
path: "testdata/both",
expectedSize: 4,
expectedContent: "foo\n",
},
{
name: "both, open compressed",
path: "testdata/both.gz",
expectedSize: 29,
// we don't check content for a explicitly compressed file
expectedContent: "",
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
f, err := testFS.Open(c.path)
if err != nil {
t.Fatal(err)
}

stat, err := f.Stat()
if err != nil {
t.Fatal(err)
}

size := stat.Size()
if size != c.expectedSize {
t.Fatalf("size is wrong, expected %d, got %d", c.expectedSize, size)
}

if strings.HasSuffix(c.path, ".gz") {
// don't read the comressed content
return
}

content, err := ioutil.ReadAll(f)
if err != nil {
t.Fatal(err)
}
if string(content) != c.expectedContent {
t.Fatalf("content is wrong, expected %s, got %s", c.expectedContent, string(content))
}
})
}
}
3 changes: 3 additions & 0 deletions assets/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/prometheus/common/assets

go 1.17
1 change: 1 addition & 0 deletions assets/testdata/both
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo
Binary file added assets/testdata/both.gz
Binary file not shown.
Binary file added assets/testdata/compressed.gz
Binary file not shown.
1 change: 1 addition & 0 deletions assets/testdata/uncompressed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo

0 comments on commit 1871a70

Please sign in to comment.