Skip to content
GitLab
Explore
Sign in
Register
Primary navigation
Search or go to…
Project
A
API
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Package registry
Container registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Terms and privacy
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
flow3r
API
Commits
bcc8c171
Commit
bcc8c171
authored
1 year ago
by
q3k
Browse files
Options
Downloads
Patches
Plain Diff
api: initial commit
parent
30d61d00
No related branches found
No related tags found
No related merge requests found
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
README.md
+28
-0
28 additions, 0 deletions
README.md
go.mod
+3
-0
3 additions, 0 deletions
go.mod
main.go
+292
-0
292 additions, 0 deletions
main.go
with
323 additions
and
0 deletions
README.md
0 → 100644
+
28
−
0
View file @
bcc8c171
flow3r api
==========
A little monolithic API service for accessing stuff about the flow3r badge,
especially release information.
Running
-------
$ go run .
APIs
----
| Endpoint | Description |
|-------------------------------------|---------------------------------------------------------|
|
`/releases.json`
| WebFlasher compatible list of releases |
|
`/release/<version>/<artifact.bin>`
| Artifacts from releases, pointed to by
`/releases.json`
|
Internals
---------
`releases.json`
is generated on demand from a cached view from the GitLab API.
Currently up to 50 releases are supported, as pagionation hasn't yet been
implemented.
All artifacts are cached in memory after being extracted from tarballs. Might
need to be offloaded to some other storage.
This diff is collapsed.
Click to expand it.
go.mod
0 → 100644
+
3
−
0
View file @
bcc8c171
module
git.flow3r.garden/flowe3r/api
go 1.20
This diff is collapsed.
Click to expand it.
main.go
0 → 100644
+
292
−
0
View file @
bcc8c171
package
main
import
(
"archive/tar"
"bytes"
"compress/bzip2"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"time"
)
var
(
flagBaseURL
string
flagListen
string
flagGitlabHost
string
flagGitlabProject
string
)
// https://git.flow3r.garden/api/v4/projects/flow3r%2Fflow3r-firmware/releases
type
GLAssetLink
struct
{
ID
int64
`json:"id"`
Name
string
`json:"name"`
TagName
string
`json:"tag_name"`
URL
string
`json:"url"`
}
type
GLRelease
struct
{
Name
string
`json:"name"`
TagName
string
`json:"tag_name"`
ReleasedAt
time
.
Time
`json:"released_at"`
Assets
struct
{
Links
[]
GLAssetLink
`json:"links"`
}
`json:"assets"`
}
func
getReleases
(
ctx
context
.
Context
)
([]
GLRelease
,
error
)
{
path
:=
fmt
.
Sprintf
(
"https://%s/api/v4/projects/%s/releases?order_by=created_at"
,
flagGitlabHost
,
url
.
PathEscape
(
flagGitlabProject
))
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
"GET"
,
path
,
nil
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"when building request: %w"
,
err
)
}
res
,
err
:=
http
.
DefaultTransport
.
RoundTrip
(
req
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"when performing request: %w"
,
err
)
}
defer
res
.
Body
.
Close
()
if
res
.
StatusCode
!=
200
{
return
nil
,
fmt
.
Errorf
(
"invalid response code %d"
,
res
.
StatusCode
)
}
var
releases
[]
GLRelease
j
:=
json
.
NewDecoder
(
res
.
Body
)
err
=
j
.
Decode
(
&
releases
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"when performing request: %w"
,
err
)
}
return
releases
,
nil
}
type
req
struct
{
getReleases
chan
[]
GLRelease
}
type
server
struct
{
reqC
chan
*
req
cacheMu
sync
.
RWMutex
cache
map
[
string
]
map
[
string
][]
byte
}
func
(
s
*
server
)
run
(
ctx
context
.
Context
)
{
releases
,
err
:=
getReleases
(
ctx
)
if
err
!=
nil
{
log
.
Fatalf
(
"Initial release fetch failed: %v"
,
err
)
}
t
:=
time
.
NewTicker
(
60
*
time
.
Second
)
for
{
select
{
case
r
:=
<-
s
.
reqC
:
if
r
.
getReleases
!=
nil
{
r
.
getReleases
<-
releases
}
case
<-
t
.
C
:
releases
,
err
=
getReleases
(
ctx
)
if
err
!=
nil
{
log
.
Printf
(
"Failed to fetch releases: %v"
,
err
)
}
}
}
}
func
(
s
*
server
)
getReleases
(
ctx
context
.
Context
)
([]
GLRelease
,
error
)
{
sresC
:=
make
(
chan
[]
GLRelease
)
select
{
case
s
.
reqC
<-
&
req
{
getReleases
:
sresC
}
:
releases
:=
<-
sresC
return
releases
,
nil
case
<-
ctx
.
Done
()
:
return
nil
,
ctx
.
Err
()
}
}
func
(
s
*
server
)
handleReleases
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
ctx
:=
r
.
Context
()
releases
,
err
:=
s
.
getReleases
(
ctx
)
if
err
!=
nil
{
return
}
type
partition
struct
{
Name
string
`json:"name"`
URL
string
`json:"url"`
Offset
string
`json:"offset"`
}
type
release
struct
{
Name
string
`json:"name"`
Partitions
[]
partition
`json:"partitions"`
}
var
resp
[]
release
for
_
,
rel
:=
range
releases
{
var
partitions
[]
partition
offsets
:=
map
[
string
]
int64
{
"bootloader"
:
0
,
"partition-table"
:
0x8000
,
"recovery"
:
0x10000
,
"flow3r"
:
0x90000
,
}
for
_
,
pname
:=
range
[]
string
{
"bootloader"
,
"partition-table"
,
"recovery"
,
"flow3r"
}
{
partitions
=
append
(
partitions
,
partition
{
Name
:
pname
,
URL
:
fmt
.
Sprintf
(
"%srelease/%s/%s.bin"
,
flagBaseURL
,
rel
.
TagName
,
pname
),
Offset
:
fmt
.
Sprintf
(
"0x%x"
,
offsets
[
pname
]),
})
}
resp
=
append
(
resp
,
release
{
Name
:
rel
.
Name
,
Partitions
:
partitions
,
})
}
w
.
Header
()
.
Add
(
"Content-Type"
,
"application/json"
)
j
:=
json
.
NewEncoder
(
w
)
j
.
Encode
(
resp
)
}
var
(
reMirrorURL
=
regexp
.
MustCompile
(
"^/release/([^/]+)/([^/]+.bin)$"
)
)
func
(
s
*
server
)
cacheTarball
(
rel
*
GLRelease
,
data
io
.
Reader
)
error
{
bz2
:=
bzip2
.
NewReader
(
data
)
t
:=
tar
.
NewReader
(
bz2
)
log
.
Printf
(
"Extracting %q..."
,
rel
.
TagName
)
cacheEntry
:=
make
(
map
[
string
][]
byte
)
for
{
hdr
,
err
:=
t
.
Next
()
if
err
==
io
.
EOF
{
break
}
if
err
!=
nil
{
return
fmt
.
Errorf
(
"when reading header: %v"
,
err
)
}
parts
:=
strings
.
Split
(
hdr
.
Name
,
"/"
)
last
:=
parts
[
len
(
parts
)
-
1
]
log
.
Printf
(
"%q has %q"
,
rel
.
TagName
,
last
)
buf
:=
bytes
.
NewBuffer
(
nil
)
if
_
,
err
:=
io
.
Copy
(
buf
,
t
);
err
!=
nil
{
return
fmt
.
Errorf
(
"file %s: %v"
,
hdr
.
Name
,
err
)
}
cacheEntry
[
last
]
=
buf
.
Bytes
()
}
s
.
cacheMu
.
Lock
()
s
.
cache
[
rel
.
TagName
]
=
cacheEntry
s
.
cacheMu
.
Unlock
()
return
nil
}
func
(
s
*
server
)
serveMirroredFile
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
,
rel
*
GLRelease
,
artifact
string
)
{
ctx
:=
r
.
Context
()
s
.
cacheMu
.
RLock
()
if
s
.
cache
[
rel
.
TagName
]
!=
nil
&&
s
.
cache
[
rel
.
TagName
][
artifact
]
!=
nil
{
b
:=
s
.
cache
[
rel
.
TagName
][
artifact
]
s
.
cacheMu
.
RUnlock
()
w
.
Header
()
.
Add
(
"Content-Type"
,
"application/octet-stream"
)
w
.
Write
(
b
)
return
}
if
s
.
cache
[
rel
.
TagName
]
!=
nil
{
log
.
Printf
(
"Failed: no %q in %q"
,
artifact
,
rel
.
TagName
)
http
.
NotFound
(
w
,
r
)
return
}
s
.
cacheMu
.
RUnlock
()
log
.
Printf
(
"Fetching %q (for %q)..."
,
rel
.
TagName
,
artifact
)
if
len
(
rel
.
Assets
.
Links
)
!=
1
{
log
.
Printf
(
"Tag %s has %d assets"
,
rel
.
TagName
,
len
(
rel
.
Assets
.
Links
))
http
.
NotFound
(
w
,
r
)
return
}
link
:=
rel
.
Assets
.
Links
[
0
]
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
"GET"
,
link
.
URL
,
nil
)
if
err
!=
nil
{
w
.
WriteHeader
(
500
)
fmt
.
Fprintf
(
w
,
"could not download"
)
return
}
res
,
err
:=
http
.
DefaultTransport
.
RoundTrip
(
req
)
if
err
!=
nil
{
w
.
WriteHeader
(
500
)
fmt
.
Fprintf
(
w
,
"could not download"
)
return
}
defer
res
.
Body
.
Close
()
if
err
:=
s
.
cacheTarball
(
rel
,
res
.
Body
);
err
!=
nil
{
w
.
WriteHeader
(
500
)
fmt
.
Fprintf
(
w
,
"could not cache"
)
return
}
s
.
cacheMu
.
RLock
()
if
s
.
cache
[
rel
.
TagName
]
!=
nil
&&
s
.
cache
[
rel
.
TagName
][
artifact
]
!=
nil
{
b
:=
s
.
cache
[
rel
.
TagName
][
artifact
]
s
.
cacheMu
.
RUnlock
()
w
.
Header
()
.
Add
(
"Content-Type"
,
"application/octet-stream"
)
w
.
Write
(
b
)
return
}
s
.
cacheMu
.
RUnlock
()
http
.
NotFound
(
w
,
r
)
}
func
(
s
*
server
)
handleReleaseMirror
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
ctx
:=
r
.
Context
()
matches
:=
reMirrorURL
.
FindStringSubmatch
(
r
.
URL
.
Path
)
if
matches
==
nil
{
http
.
NotFound
(
w
,
r
)
return
}
tag
:=
matches
[
1
]
artifact
:=
matches
[
2
]
releases
,
err
:=
s
.
getReleases
(
ctx
)
if
err
!=
nil
{
return
}
for
_
,
rel
:=
range
releases
{
if
rel
.
TagName
==
tag
{
s
.
serveMirroredFile
(
w
,
r
,
&
rel
,
artifact
)
return
}
}
}
func
main
()
{
flag
.
StringVar
(
&
flagBaseURL
,
"base_url"
,
"https://flow3r.garden/api/"
,
"Base address at which this instance runs (used for calculating proxied data URLs)"
)
flag
.
StringVar
(
&
flagListen
,
"listen"
,
":8080"
,
"Address on which to listen"
)
flag
.
StringVar
(
&
flagGitlabHost
,
"gitlab_host"
,
"git.flow3r.garden"
,
"GitLab instance host"
)
flag
.
StringVar
(
&
flagGitlabProject
,
"gitlab_project"
,
"flow3r/flow3r-firmware"
,
"Name of the organization/project on GitLab"
)
flag
.
Parse
()
ctx
:=
context
.
Background
()
s
:=
server
{
reqC
:
make
(
chan
*
req
),
cache
:
make
(
map
[
string
]
map
[
string
][]
byte
),
}
go
s
.
run
(
ctx
)
http
.
HandleFunc
(
"/releases.json"
,
s
.
handleReleases
)
http
.
HandleFunc
(
"/release/"
,
s
.
handleReleaseMirror
)
log
.
Printf
(
"Listening on %s..."
,
flagListen
)
http
.
ListenAndServe
(
flagListen
,
nil
)
}
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment