Skip to content

Commit

Permalink
Merge pull request #41 from dadav/feat/improve_ui
Browse files Browse the repository at this point in the history
feat(ui): Add sorting support and add proxy filter
  • Loading branch information
dadav authored Dec 29, 2024
2 parents 306fea0 + 6a2378d commit 5b02c88
Show file tree
Hide file tree
Showing 17 changed files with 200 additions and 120 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ Flags:
--dev enables dev mode
--drop-privileges drops privileges to the given user/group
--fallback-proxy string optional comma separated list of fallback upstream proxy urls
--proxy-prefixes string url prefixes to proxy (default "/v3")
--group string give control to this group or gid (requires root)
-h, --help help for serve
--import-proxied-releases add every proxied modules to local store
Expand Down Expand Up @@ -177,6 +178,8 @@ dev: false
drop-privileges: false
# List of comma separated upstream forge(s) to use when local requests return 404
fallback-proxy:
# The prefixes of requests to send to the proxies. Multiple entries must be separated by comma.
proxy-prefixes: /v3
# Import proxied modules into local backend.
import-proxied-releases: false
# Path to local modules.
Expand Down Expand Up @@ -213,6 +216,7 @@ GORGE_CORS="*"
GORGE_DEV=false
GORGE_DROP_PRIVILEGES=false
GORGE_FALLBACK_PROXY=""
GORGE_PROXY_PREFIXES=/v3
GORGE_IMPORT_PROXIED_RELEASES=false
GORGE_MODULESDIR=~/.gorge/modules
GORGE_MODULES_SCAN_SEC=0
Expand Down
14 changes: 12 additions & 2 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,20 @@ You can also enable the caching functionality to speed things up.`,
proxies := strings.Split(config.FallbackProxyUrl, ",")
slices.Reverse(proxies)

proxyPrefixes := strings.Split(config.ProxyPrefixes, ",")

for _, proxy := range proxies {
r.Use(customMiddleware.ProxyFallback(
proxy,
func(status int) bool {
return status == http.StatusNotFound
func(r *http.Request, status int) bool {
shouldProxy := false
for _, prefix := range proxyPrefixes {
if strings.HasPrefix(r.URL.Path, strings.TrimSpace(prefix)) {
shouldProxy = true
break
}
}
return shouldProxy && status == http.StatusNotFound
},
func(r *http.Response) {
r.Header.Add("X-Proxied-To", proxy)
Expand Down Expand Up @@ -395,6 +404,7 @@ func init() {
serveCmd.Flags().BoolVar(&config.DropPrivileges, "drop-privileges", false, "drops privileges to the given user/group")
serveCmd.Flags().BoolVar(&config.UI, "ui", false, "enables the web ui")
serveCmd.Flags().StringVar(&config.CachePrefixes, "cache-prefixes", "/v3/files", "url prefixes to cache")
serveCmd.Flags().StringVar(&config.ProxyPrefixes, "proxy-prefixes", "/v3", "url prefixes to proxy")
serveCmd.Flags().StringVar(&config.JwtSecret, "jwt-secret", "changeme", "jwt secret")
serveCmd.Flags().StringVar(&config.JwtTokenPath, "jwt-token-path", "~/.gorge/token", "jwt token path")
serveCmd.Flags().StringVar(&config.TlsCertPath, "tls-cert", "", "path to tls cert file")
Expand Down
2 changes: 2 additions & 0 deletions defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ dev: false
drop-privileges: false
# List of comma separated upstream forge(s) to use when local requests return 404
fallback-proxy:
# The prefixes of requests to send to the proxies. Multiple entries must be separated by comma.
proxy-prefixes: /v3
# Import proxied modules into local backend.
import-proxied-releases: false
# Path to local modules.
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
FallbackProxyUrl string
NoCache bool
CachePrefixes string
ProxyPrefixes string
CacheByFullRequestURI bool
CacheMaxAge int64
ImportProxiedReleases bool
Expand Down
4 changes: 2 additions & 2 deletions internal/middleware/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func NewSingleHostReverseProxy(target *url.URL) *httputil.ReverseProxy {
}
}

func ProxyFallback(upstreamHost string, forwardToProxy func(int) bool, proxiedResponseCb func(*http.Response)) func(next http.Handler) http.Handler {
func ProxyFallback(upstreamHost string, forwardToProxy func(*http.Request, int) bool, proxiedResponseCb func(*http.Response)) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Store original headers before any modifications
Expand All @@ -58,7 +58,7 @@ func ProxyFallback(upstreamHost string, forwardToProxy func(int) bool, proxiedRe
capturedResponseWriter := NewCapturedResponseWriter(w)
next.ServeHTTP(capturedResponseWriter, r)

if forwardToProxy(capturedResponseWriter.status) {
if forwardToProxy(r, capturedResponseWriter.status) {
log.Log.Infof("Forwarding request to %s\n", upstreamHost)
u, err := url.Parse(upstreamHost)
if err != nil {
Expand Down
80 changes: 24 additions & 56 deletions internal/v3/backend/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/hashicorp/go-version"
)

// FilesystemBackend implements the Backend interface for local filesystem storage
type FilesystemBackend struct {
muModules sync.RWMutex
Modules map[string]*gen.Module
Expand All @@ -49,6 +50,7 @@ func NewFilesystemBackend(path string) *FilesystemBackend {
}
}

// findLatestVersion compares version strings and returns the most recent one
func findLatestVersion(releases []gen.ReleaseAbbreviated) string {
if len(releases) == 0 {
return defaultVersion
Expand Down Expand Up @@ -100,6 +102,7 @@ func (s *FilesystemBackend) GetAllReleases() ([]*gen.Release, error) {
return result, nil
}

// MetadataToRelease converts release metadata into a Release object
func MetadataToRelease(metadata *model.ReleaseMetadata) *gen.Release {
var releaseMetadataInterface map[string]interface{}
inrec, _ := json.Marshal(metadata)
Expand Down Expand Up @@ -345,29 +348,34 @@ func (s *FilesystemBackend) DeleteReleaseBySlug(slug string) error {
}

func (s *FilesystemBackend) LoadModules() error {
// don't overwrite data we already have on modules and releases
// Initialize maps if they haven't been created yet
if s.Modules == nil {
s.Modules = make(map[string]*gen.Module)
}
if s.Releases == nil {
s.Releases = make(map[string][]*gen.Release)
}

// Walk through all files in the modules directory recursively
err := filepath.Walk(s.ModulesDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

// Skip directories and non-tar.gz files
if info.IsDir() || !strings.HasSuffix(info.Name(), ".tar.gz") {
return nil
}

log.Log.Debugf("Reading %s\n", path)
// Read the release archive file
releaseBytes, err := os.ReadFile(path)
if err != nil {
return err
}

// Process the release archive and add it to the backend
// This will update both s.Modules and s.Releases maps
_, err = s.AddRelease(releaseBytes)
return err
})
Expand All @@ -377,61 +385,14 @@ func (s *FilesystemBackend) LoadModules() error {
return nil
}

func ReadReleaseMetadataFromFile(path string) (*model.ReleaseMetadata, string, error) {
var jsonData bytes.Buffer
var releaseMetadata model.ReleaseMetadata
readme := new(strings.Builder)

f, err := os.Open(path)
if err != nil {
return nil, readme.String(), err
}
defer f.Close()

g, err := gzip.NewReader(f)
if err != nil {
return nil, readme.String(), err
}

tarReader := tar.NewReader(g)

for {
header, err := tarReader.Next()
if err == io.EOF {
break
}

if err != nil {
return nil, readme.String(), err
}

if header.Typeflag != tar.TypeReg {
continue
}

switch filepath.Base(header.Name) {
case "metadata.json":
_, err = io.Copy(&jsonData, tarReader)
if err != nil {
return nil, readme.String(), err
}

if err := json.Unmarshal(jsonData.Bytes(), &releaseMetadata); err != nil {
return nil, readme.String(), err
}

case "README.md":
_, err = io.Copy(readme, tarReader)
if err != nil {
return nil, readme.String(), err
}
default:
continue
}
}
return &releaseMetadata, readme.String(), nil
}

// ReadReleaseMetadataFromBytes extracts metadata and README from a gzipped tar archive
// Parameters:
// - data: byte slice containing the gzipped tar archive
//
// Returns:
// - *model.ReleaseMetadata: parsed metadata from metadata.json
// - string: contents of README.md
// - error: any errors encountered during processing
func ReadReleaseMetadataFromBytes(data []byte) (*model.ReleaseMetadata, string, error) {
if len(data) == 0 {
return nil, "", errors.New("empty data provided")
Expand All @@ -441,6 +402,7 @@ func ReadReleaseMetadataFromBytes(data []byte) (*model.ReleaseMetadata, string,
var releaseMetadata model.ReleaseMetadata
readme := new(strings.Builder)

// Create readers to process the gzipped tar data
f := bytes.NewReader(data)
g, err := gzip.NewReader(f)
if err != nil {
Expand All @@ -450,6 +412,7 @@ func ReadReleaseMetadataFromBytes(data []byte) (*model.ReleaseMetadata, string,

tarReader := tar.NewReader(g)

// Iterate through all files in the archive
for {
header, err := tarReader.Next()
if err == io.EOF {
Expand All @@ -460,12 +423,15 @@ func ReadReleaseMetadataFromBytes(data []byte) (*model.ReleaseMetadata, string,
return nil, readme.String(), err
}

// Skip if not a regular file
if header.Typeflag != tar.TypeReg {
continue
}

// Process only metadata.json and README.md files
switch filepath.Base(header.Name) {
case "metadata.json":
// Read and parse the metadata file
_, err = io.Copy(&jsonData, tarReader)
if err != nil {
return nil, readme.String(), err
Expand All @@ -475,10 +441,12 @@ func ReadReleaseMetadataFromBytes(data []byte) (*model.ReleaseMetadata, string,
return nil, readme.String(), err
}

// Validate the module name
if !utils.CheckModuleSlug(releaseMetadata.Name) {
return nil, readme.String(), errors.New("invalid module name")
}
case "README.md":
// Read the README contents
_, err = io.Copy(readme, tarReader)
if err != nil {
return nil, readme.String(), err
Expand Down
49 changes: 49 additions & 0 deletions internal/v3/ui/assets/js/table-sort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
function sortTable(tableId, columnIndex) {
var table = document.getElementById(tableId);
var switching = true;
var dir = "asc";
var switchcount = 0;

while (switching) {
switching = false;
var rows = table.rows;

for (var i = 1; i < (rows.length - 1); i++) {
var shouldSwitch = false;
var x = rows[i].getElementsByTagName("TD")[columnIndex];
var y = rows[i + 1].getElementsByTagName("TD")[columnIndex];

var xContent = x.innerHTML.toLowerCase();
var yContent = y.innerHTML.toLowerCase();

// Try to convert to numbers if the content looks numeric
if (xContent.match(/^[\d./]+$/) && yContent.match(/^[\d./]+$/)) {
xContent = parseFloat(xContent.replace(/[^\d.-]/g, '')) || xContent;
yContent = parseFloat(yContent.replace(/[^\d.-]/g, '')) || yContent;
}

if (dir === "asc") {
if (xContent > yContent) {
shouldSwitch = true;
break;
}
} else if (dir === "desc") {
if (xContent < yContent) {
shouldSwitch = true;
break;
}
}
}

if (shouldSwitch) {
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
switchcount++;
} else {
if (switchcount === 0 && dir === "asc") {
dir = "desc";
switching = true;
}
}
}
}
4 changes: 4 additions & 0 deletions internal/v3/ui/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
max-width: 50%;
}

th[onclick] {
cursor: pointer;
}

/* https://codepen.io/irunatbullets/pen/MWwyVOw */
.switch {
display: inline-block;
Expand Down
9 changes: 5 additions & 4 deletions internal/v3/ui/components/author.templ
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import (
templ AuthorView(modules []*gen.Module) {
<h3>{ modules[0].Owner.Username }</h3>
<h4>Modules</h4>
<table>
<table id="modulesTable">
<thead>
<tr>
<th scope="col">Module</th>
<th scope="col">Version</th>
<th scope="col" onclick="sortTable('modulesTable', 0)">Module</th>
<th scope="col" onclick="sortTable('modulesTable', 1)">Version</th>
</tr>
</thead>
<tbody>
for _, module := range modules {
for _, module := range sortModules(modules) {
<tr>
<td>
<a href={ templ.URL(fmt.Sprintf("/modules/%s", module.Slug)) }>{ module.Name }</a>
Expand All @@ -28,4 +28,5 @@ templ AuthorView(modules []*gen.Module) {
}
</tbody>
</table>
<script src="/assets/js/table-sort.js"></script>
}
6 changes: 3 additions & 3 deletions internal/v3/ui/components/author_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5b02c88

Please sign in to comment.