2023-04-28 23:57:40 +08:00
package artifactcache
import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
2025-09-04 16:55:26 +02:00
"syscall"
2023-04-28 23:57:40 +08:00
"time"
"github.com/julienschmidt/httprouter"
"github.com/sirupsen/logrus"
"github.com/timshannon/bolthold"
2025-09-05 07:29:38 +00:00
"code.forgejo.org/forgejo/runner/v11/act/common"
2023-04-28 23:57:40 +08:00
)
const (
urlBase = "/_apis/artifactcache"
)
2025-09-04 16:55:26 +02:00
var fatal = func ( logger logrus . FieldLogger , err error ) {
logger . Errorf ( "unrecoverable error in the cache: %v" , err )
if err := syscall . Kill ( syscall . Getpid ( ) , syscall . SIGTERM ) ; err != nil {
logger . Errorf ( "unrecoverable error in the cache: failed to send the TERM signal to shutdown the daemon %v" , err )
}
}
2025-09-04 14:38:50 +00:00
type Handler interface {
ExternalURL ( ) string
Close ( ) error
isClosed ( ) bool
2025-09-05 11:17:57 +02:00
getCaches ( ) caches
setCaches ( caches caches )
2025-09-04 14:38:50 +00:00
find ( w http . ResponseWriter , r * http . Request , params httprouter . Params )
reserve ( w http . ResponseWriter , r * http . Request , params httprouter . Params )
upload ( w http . ResponseWriter , r * http . Request , params httprouter . Params )
commit ( w http . ResponseWriter , r * http . Request , params httprouter . Params )
get ( w http . ResponseWriter , r * http . Request , params httprouter . Params )
clean ( w http . ResponseWriter , r * http . Request , _ httprouter . Params )
middleware ( handler httprouter . Handle ) httprouter . Handle
responseJSON ( w http . ResponseWriter , r * http . Request , code int , v ... any )
}
type handler struct {
2025-09-05 11:17:57 +02:00
caches caches
2023-04-28 23:57:40 +08:00
router * httprouter . Router
listener net . Listener
server * http . Server
logger logrus . FieldLogger
outboundIP string
}
2025-09-04 14:38:50 +00:00
func StartHandler ( dir , outboundIP string , port uint16 , secret string , logger logrus . FieldLogger ) ( Handler , error ) {
2025-09-05 11:17:57 +02:00
h := & handler { }
2023-04-28 23:57:40 +08:00
if logger == nil {
discard := logrus . New ( )
discard . Out = io . Discard
logger = discard
}
logger = logger . WithField ( "module" , "artifactcache" )
h . logger = logger
2025-09-05 11:17:57 +02:00
caches , err := newCaches ( dir , secret , logger )
2023-04-28 23:57:40 +08:00
if err != nil {
return nil , err
}
2025-09-05 11:17:57 +02:00
h . caches = caches
2023-04-28 23:57:40 +08:00
if outboundIP != "" {
h . outboundIP = outboundIP
} else if ip := common . GetOutboundIP ( ) ; ip == nil {
return nil , fmt . Errorf ( "unable to determine outbound IP address" )
} else {
h . outboundIP = ip . String ( )
}
router := httprouter . New ( )
2024-12-07 17:48:07 +01:00
router . GET ( urlBase + "/cache" , h . middleware ( h . find ) )
router . POST ( urlBase + "/caches" , h . middleware ( h . reserve ) )
router . PATCH ( urlBase + "/caches/:id" , h . middleware ( h . upload ) )
router . POST ( urlBase + "/caches/:id" , h . middleware ( h . commit ) )
router . GET ( urlBase + "/artifacts/:id" , h . middleware ( h . get ) )
router . POST ( urlBase + "/clean" , h . middleware ( h . clean ) )
2023-04-28 23:57:40 +08:00
h . router = router
listener , err := net . Listen ( "tcp" , fmt . Sprintf ( ":%d" , port ) ) // listen on all interfaces
if err != nil {
return nil , err
}
server := & http . Server {
ReadHeaderTimeout : 2 * time . Second ,
Handler : router ,
}
go func ( ) {
if err := server . Serve ( listener ) ; err != nil && errors . Is ( err , net . ErrClosed ) {
logger . Errorf ( "http serve: %v" , err )
}
} ( )
h . listener = listener
h . server = server
return h , nil
}
2025-09-04 14:38:50 +00:00
func ( h * handler ) ExternalURL ( ) string {
2025-05-25 19:16:18 +02:00
port := strconv . Itoa ( h . listener . Addr ( ) . ( * net . TCPAddr ) . Port )
2023-04-28 23:57:40 +08:00
// TODO: make the external url configurable if necessary
2025-05-25 19:16:18 +02:00
return fmt . Sprintf ( "http://%s" , net . JoinHostPort ( h . outboundIP , port ) )
2023-04-28 23:57:40 +08:00
}
2025-09-04 14:38:50 +00:00
func ( h * handler ) Close ( ) error {
2023-04-28 23:57:40 +08:00
if h == nil {
return nil
}
var retErr error
if h . server != nil {
err := h . server . Close ( )
if err != nil {
retErr = err
}
h . server = nil
}
if h . listener != nil {
err := h . listener . Close ( )
if errors . Is ( err , net . ErrClosed ) {
err = nil
}
if err != nil {
retErr = err
}
h . listener = nil
}
return retErr
}
2025-09-04 14:38:50 +00:00
func ( h * handler ) isClosed ( ) bool {
return h . listener == nil && h . server == nil
}
2025-09-05 11:17:57 +02:00
func ( h * handler ) getCaches ( ) caches {
return h . caches
}
func ( h * handler ) setCaches ( caches caches ) {
h . caches = caches
2023-07-10 18:57:06 +02:00
}
2023-04-28 23:57:40 +08:00
// GET /_apis/artifactcache/cache
2025-09-04 14:38:50 +00:00
func ( h * handler ) find ( w http . ResponseWriter , r * http . Request , params httprouter . Params ) {
2024-12-07 17:48:07 +01:00
rundata := runDataFromHeaders ( r )
2025-09-05 11:17:57 +02:00
repo , err := h . caches . validateMac ( rundata )
2024-11-21 22:49:12 +01:00
if err != nil {
2025-01-13 16:59:07 +01:00
h . responseJSON ( w , r , 403 , err )
2024-11-21 22:49:12 +01:00
return
}
2023-04-28 23:57:40 +08:00
keys := strings . Split ( r . URL . Query ( ) . Get ( "keys" ) , "," )
// cache keys are case insensitive
for i , key := range keys {
keys [ i ] = strings . ToLower ( key )
}
version := r . URL . Query ( ) . Get ( "version" )
2025-09-05 11:17:57 +02:00
db , err := h . caches . openDB ( )
2023-07-10 18:57:06 +02:00
if err != nil {
2025-09-01 13:37:57 +02:00
h . responseFatalJSON ( w , r , err )
2023-07-10 18:57:06 +02:00
return
}
defer db . Close ( )
2025-09-05 17:21:53 +02:00
cache , err := findCacheWithIsolationKeyFallback ( db , repo , keys , version , rundata . WriteIsolationKey )
2023-04-28 23:57:40 +08:00
if err != nil {
2025-09-01 13:37:57 +02:00
h . responseFatalJSON ( w , r , err )
2023-04-28 23:57:40 +08:00
return
}
if cache == nil {
h . responseJSON ( w , r , 204 )
return
}
2025-09-05 11:17:57 +02:00
if ok , err := h . caches . exist ( cache . ID ) ; err != nil {
2023-04-28 23:57:40 +08:00
h . responseJSON ( w , r , 500 , err )
return
} else if ! ok {
2023-07-10 18:57:06 +02:00
_ = db . Delete ( cache . ID , cache )
2023-04-28 23:57:40 +08:00
h . responseJSON ( w , r , 204 )
return
}
2025-01-13 16:50:45 +01:00
archiveLocation := fmt . Sprintf ( "%s/%s%s/artifacts/%d" , r . Header . Get ( "Forgejo-Cache-Host" ) , r . Header . Get ( "Forgejo-Cache-RunId" ) , urlBase , cache . ID )
2023-04-28 23:57:40 +08:00
h . responseJSON ( w , r , 200 , map [ string ] any {
"result" : "hit" ,
2025-01-13 16:50:45 +01:00
"archiveLocation" : archiveLocation ,
2023-04-28 23:57:40 +08:00
"cacheKey" : cache . Key ,
} )
}
// POST /_apis/artifactcache/caches
2025-09-04 14:38:50 +00:00
func ( h * handler ) reserve ( w http . ResponseWriter , r * http . Request , params httprouter . Params ) {
2024-12-07 17:48:07 +01:00
rundata := runDataFromHeaders ( r )
2025-09-05 11:17:57 +02:00
repo , err := h . caches . validateMac ( rundata )
2024-11-21 22:49:12 +01:00
if err != nil {
2025-01-13 16:59:07 +01:00
h . responseJSON ( w , r , 403 , err )
2024-11-21 22:49:12 +01:00
return
}
2023-04-28 23:57:40 +08:00
api := & Request { }
if err := json . NewDecoder ( r . Body ) . Decode ( api ) ; err != nil {
h . responseJSON ( w , r , 400 , err )
return
}
// cache keys are case insensitive
api . Key = strings . ToLower ( api . Key )
cache := api . ToCache ( )
2025-09-05 11:17:57 +02:00
db , err := h . caches . openDB ( )
2023-07-10 18:57:06 +02:00
if err != nil {
2025-09-01 13:37:57 +02:00
h . responseFatalJSON ( w , r , err )
2023-07-10 18:57:06 +02:00
return
}
defer db . Close ( )
2023-04-28 23:57:40 +08:00
now := time . Now ( ) . Unix ( )
cache . CreatedAt = now
cache . UsedAt = now
2024-11-21 22:49:12 +01:00
cache . Repo = repo
2025-08-15 20:26:35 -06:00
cache . WriteIsolationKey = rundata . WriteIsolationKey
2024-03-29 00:42:02 +08:00
if err := insertCache ( db , cache ) ; err != nil {
2023-04-28 23:57:40 +08:00
h . responseJSON ( w , r , 500 , err )
return
}
h . responseJSON ( w , r , 200 , map [ string ] any {
"cacheId" : cache . ID ,
} )
}
// PATCH /_apis/artifactcache/caches/:id
2025-09-04 14:38:50 +00:00
func ( h * handler ) upload ( w http . ResponseWriter , r * http . Request , params httprouter . Params ) {
2024-12-07 17:48:07 +01:00
rundata := runDataFromHeaders ( r )
2025-09-05 11:17:57 +02:00
repo , err := h . caches . validateMac ( rundata )
2024-11-21 22:49:12 +01:00
if err != nil {
2025-01-13 16:59:07 +01:00
h . responseJSON ( w , r , 403 , err )
2024-11-21 22:49:12 +01:00
return
}
2024-11-22 01:01:12 +01:00
id , err := strconv . ParseUint ( params . ByName ( "id" ) , 10 , 64 )
2023-04-28 23:57:40 +08:00
if err != nil {
h . responseJSON ( w , r , 400 , err )
return
}
2025-09-05 15:00:38 +02:00
cache , err := h . caches . readCache ( id , repo )
2023-07-10 18:57:06 +02:00
if err != nil {
2023-04-28 23:57:40 +08:00
if errors . Is ( err , bolthold . ErrNotFound ) {
2025-03-24 10:48:28 +01:00
h . responseJSON ( w , r , 404 , fmt . Errorf ( "cache %d: not reserved" , id ) )
2023-04-28 23:57:40 +08:00
return
}
2025-09-01 13:37:57 +02:00
h . responseFatalJSON ( w , r , fmt . Errorf ( "cache Get: %w" , err ) )
2023-04-28 23:57:40 +08:00
return
}
2025-08-15 20:26:35 -06:00
if cache . WriteIsolationKey != rundata . WriteIsolationKey {
h . responseJSON ( w , r , 403 , fmt . Errorf ( "cache authorized for write isolation %q, but attempting to operate on %q" , rundata . WriteIsolationKey , cache . WriteIsolationKey ) )
return
}
2024-11-21 22:49:12 +01:00
2023-04-28 23:57:40 +08:00
if cache . Complete {
h . responseJSON ( w , r , 400 , fmt . Errorf ( "cache %v %q: already complete" , cache . ID , cache . Key ) )
return
}
start , _ , err := parseContentRange ( r . Header . Get ( "Content-Range" ) )
if err != nil {
2025-03-23 22:31:16 +01:00
h . responseJSON ( w , r , 400 , fmt . Errorf ( "cache parseContentRange(%s): %w" , r . Header . Get ( "Content-Range" ) , err ) )
2023-04-28 23:57:40 +08:00
return
}
2025-09-05 11:17:57 +02:00
if err := h . caches . write ( cache . ID , start , r . Body ) ; err != nil {
2025-03-23 22:31:16 +01:00
h . responseJSON ( w , r , 500 , fmt . Errorf ( "cache storage.Write: %w" , err ) )
return
}
2025-09-05 11:17:57 +02:00
if err := h . caches . useCache ( id ) ; err != nil {
2025-03-23 22:31:16 +01:00
h . responseJSON ( w , r , 500 , fmt . Errorf ( "cache useCache: %w" , err ) )
return
2023-04-28 23:57:40 +08:00
}
h . responseJSON ( w , r , 200 )
}
// POST /_apis/artifactcache/caches/:id
2025-09-04 14:38:50 +00:00
func ( h * handler ) commit ( w http . ResponseWriter , r * http . Request , params httprouter . Params ) {
2024-12-07 17:48:07 +01:00
rundata := runDataFromHeaders ( r )
2025-09-05 11:17:57 +02:00
repo , err := h . caches . validateMac ( rundata )
2024-11-21 22:49:12 +01:00
if err != nil {
2025-01-13 16:59:07 +01:00
h . responseJSON ( w , r , 403 , err )
2024-11-21 22:49:12 +01:00
return
}
2024-11-22 01:01:12 +01:00
id , err := strconv . ParseUint ( params . ByName ( "id" ) , 10 , 64 )
2023-04-28 23:57:40 +08:00
if err != nil {
h . responseJSON ( w , r , 400 , err )
return
}
2025-09-05 15:00:38 +02:00
cache , err := h . caches . readCache ( id , repo )
2023-07-10 18:57:06 +02:00
if err != nil {
2023-04-28 23:57:40 +08:00
if errors . Is ( err , bolthold . ErrNotFound ) {
2025-03-24 10:48:28 +01:00
h . responseJSON ( w , r , 404 , fmt . Errorf ( "cache %d: not reserved" , id ) )
2023-04-28 23:57:40 +08:00
return
}
2025-09-01 13:37:57 +02:00
h . responseFatalJSON ( w , r , fmt . Errorf ( "cache Get: %w" , err ) )
2023-04-28 23:57:40 +08:00
return
}
2025-08-15 20:26:35 -06:00
if cache . WriteIsolationKey != rundata . WriteIsolationKey {
h . responseJSON ( w , r , 403 , fmt . Errorf ( "cache authorized for write isolation %q, but attempting to operate on %q" , rundata . WriteIsolationKey , cache . WriteIsolationKey ) )
return
}
2024-11-21 22:49:12 +01:00
2023-04-28 23:57:40 +08:00
if cache . Complete {
h . responseJSON ( w , r , 400 , fmt . Errorf ( "cache %v %q: already complete" , cache . ID , cache . Key ) )
return
}
2025-09-05 11:17:57 +02:00
size , err := h . caches . commit ( cache . ID , cache . Size )
2023-07-11 11:35:27 +08:00
if err != nil {
2025-09-01 13:37:57 +02:00
h . responseJSON ( w , r , 500 , fmt . Errorf ( "commit(%v): %w" , cache . ID , err ) )
2023-04-28 23:57:40 +08:00
return
}
2023-07-11 11:35:27 +08:00
// write real size back to cache, it may be different from the current value when the request doesn't specify it.
cache . Size = size
2023-04-28 23:57:40 +08:00
2025-09-05 11:17:57 +02:00
db , err := h . caches . openDB ( )
2023-07-10 18:57:06 +02:00
if err != nil {
2025-09-01 13:37:57 +02:00
h . responseFatalJSON ( w , r , err )
2023-07-10 18:57:06 +02:00
return
}
defer db . Close ( )
2023-04-28 23:57:40 +08:00
cache . Complete = true
2023-07-10 18:57:06 +02:00
if err := db . Update ( cache . ID , cache ) ; err != nil {
2023-04-28 23:57:40 +08:00
h . responseJSON ( w , r , 500 , err )
return
}
h . responseJSON ( w , r , 200 )
}
// GET /_apis/artifactcache/artifacts/:id
2025-09-04 14:38:50 +00:00
func ( h * handler ) get ( w http . ResponseWriter , r * http . Request , params httprouter . Params ) {
2024-12-07 17:48:07 +01:00
rundata := runDataFromHeaders ( r )
2025-09-05 11:17:57 +02:00
repo , err := h . caches . validateMac ( rundata )
2024-11-21 22:49:12 +01:00
if err != nil {
2025-01-13 16:59:07 +01:00
h . responseJSON ( w , r , 403 , err )
2024-11-21 22:49:12 +01:00
return
}
2024-11-22 01:01:12 +01:00
id , err := strconv . ParseUint ( params . ByName ( "id" ) , 10 , 64 )
2023-04-28 23:57:40 +08:00
if err != nil {
h . responseJSON ( w , r , 400 , err )
return
}
2024-11-21 22:49:12 +01:00
2025-09-05 15:00:38 +02:00
cache , err := h . caches . readCache ( id , repo )
2025-03-24 10:48:28 +01:00
if err != nil {
if errors . Is ( err , bolthold . ErrNotFound ) {
h . responseJSON ( w , r , 404 , fmt . Errorf ( "cache %d: not reserved" , id ) )
2024-11-21 22:49:12 +01:00
return
}
2025-09-01 13:37:57 +02:00
h . responseFatalJSON ( w , r , fmt . Errorf ( "cache Get: %w" , err ) )
2025-03-24 10:48:28 +01:00
return
2024-11-21 22:49:12 +01:00
}
2025-08-15 20:26:35 -06:00
// reads permitted against caches w/ the same isolation key, or no isolation key
if cache . WriteIsolationKey != rundata . WriteIsolationKey && cache . WriteIsolationKey != "" {
h . responseJSON ( w , r , 403 , fmt . Errorf ( "cache authorized for write isolation %q, but attempting to operate on %q" , rundata . WriteIsolationKey , cache . WriteIsolationKey ) )
return
}
2024-11-21 22:49:12 +01:00
2025-09-05 11:17:57 +02:00
if err := h . caches . useCache ( id ) ; err != nil {
2025-03-23 22:31:16 +01:00
h . responseJSON ( w , r , 500 , fmt . Errorf ( "cache useCache: %w" , err ) )
return
}
2025-09-05 11:17:57 +02:00
h . caches . serve ( w , r , id )
2023-04-28 23:57:40 +08:00
}
// POST /_apis/artifactcache/clean
2025-09-04 14:38:50 +00:00
func ( h * handler ) clean ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
2024-12-07 17:48:07 +01:00
rundata := runDataFromHeaders ( r )
2025-09-05 11:17:57 +02:00
_ , err := h . caches . validateMac ( rundata )
2024-11-21 22:49:12 +01:00
if err != nil {
2025-01-13 16:59:07 +01:00
h . responseJSON ( w , r , 403 , err )
2024-11-21 22:49:12 +01:00
return
}
2023-04-28 23:57:40 +08:00
// TODO: don't support force deleting cache entries
// see: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
h . responseJSON ( w , r , 200 )
}
2025-09-04 14:38:50 +00:00
func ( h * handler ) middleware ( handler httprouter . Handle ) httprouter . Handle {
2023-04-28 23:57:40 +08:00
return func ( w http . ResponseWriter , r * http . Request , params httprouter . Params ) {
h . logger . Debugf ( "%s %s" , r . Method , r . RequestURI )
handler ( w , r , params )
2025-09-05 11:17:57 +02:00
go h . caches . gcCache ( )
2024-03-29 00:42:02 +08:00
}
2023-04-28 23:57:40 +08:00
}
2025-09-01 13:37:57 +02:00
func ( h * handler ) responseFatalJSON ( w http . ResponseWriter , r * http . Request , err error ) {
h . responseJSON ( w , r , 500 , err )
fatal ( h . logger , err )
}
2025-09-04 14:38:50 +00:00
func ( h * handler ) responseJSON ( w http . ResponseWriter , r * http . Request , code int , v ... any ) {
2023-04-28 23:57:40 +08:00
w . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
var data [ ] byte
if len ( v ) == 0 || v [ 0 ] == nil {
data , _ = json . Marshal ( struct { } { } )
} else if err , ok := v [ 0 ] . ( error ) ; ok {
h . logger . Errorf ( "%v %v: %v" , r . Method , r . RequestURI , err )
data , _ = json . Marshal ( map [ string ] any {
"error" : err . Error ( ) ,
} )
} else {
data , _ = json . Marshal ( v [ 0 ] )
}
w . WriteHeader ( code )
_ , _ = w . Write ( data )
}
2024-11-22 01:01:12 +01:00
func parseContentRange ( s string ) ( uint64 , uint64 , error ) {
2023-04-28 23:57:40 +08:00
// support the format like "bytes 11-22/*" only
s , _ , _ = strings . Cut ( strings . TrimPrefix ( s , "bytes " ) , "/" )
s1 , s2 , _ := strings . Cut ( s , "-" )
2024-11-22 01:01:12 +01:00
start , err := strconv . ParseUint ( s1 , 10 , 64 )
2023-04-28 23:57:40 +08:00
if err != nil {
return 0 , 0 , fmt . Errorf ( "parse %q: %w" , s , err )
}
2024-11-22 01:01:12 +01:00
stop , err := strconv . ParseUint ( s2 , 10 , 64 )
2023-04-28 23:57:40 +08:00
if err != nil {
return 0 , 0 , fmt . Errorf ( "parse %q: %w" , s , err )
}
return start , stop , nil
}
2024-12-07 17:48:07 +01:00
2025-09-05 06:01:49 +00:00
type RunData struct {
RepositoryFullName string
RunNumber string
Timestamp string
RepositoryMAC string
WriteIsolationKey string
}
func runDataFromHeaders ( r * http . Request ) RunData {
return RunData {
2024-12-07 17:48:07 +01:00
RepositoryFullName : r . Header . Get ( "Forgejo-Cache-Repo" ) ,
RunNumber : r . Header . Get ( "Forgejo-Cache-RunNumber" ) ,
Timestamp : r . Header . Get ( "Forgejo-Cache-Timestamp" ) ,
RepositoryMAC : r . Header . Get ( "Forgejo-Cache-MAC" ) ,
2025-08-15 20:26:35 -06:00
WriteIsolationKey : r . Header . Get ( "Forgejo-Cache-WriteIsolationKey" ) ,
2024-12-07 17:48:07 +01:00
}
}