@ -24,8 +24,15 @@ package actions
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=block
// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock
// 1.4. Unknown xml payload to Blobstorage (unauthenticated request), ignored for now
// 1.4. BlockList xml payload to Blobstorage (unauthenticated request)
// Files of about 800MB are parallel in parallel and / or out of order, this file is needed to enshure the correct order
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList
// Request
// <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
// <BlockList>
// <Latest>blockId1</Latest>
// <Latest>blockId2</Latest>
// </BlockList>
// 1.5. FinalizeArtifact
// Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact
// Request
@ -82,6 +89,7 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/xml"
"fmt"
"io"
"net/http"
@ -152,31 +160,34 @@ func ArtifactsV4Routes(prefix string) *web.Router {
return m
}
func ( r artifactV4Routes ) buildSignature ( endp , expires , artifactName string , taskID int64 ) [ ] byte {
func ( r artifactV4Routes ) buildSignature ( endp , expires , artifactName string , taskID , artifactID int64 ) [ ] byte {
mac := hmac . New ( sha256 . New , setting . GetGeneralTokenSigningSecret ( ) )
mac . Write ( [ ] byte ( endp ) )
mac . Write ( [ ] byte ( expires ) )
mac . Write ( [ ] byte ( artifactName ) )
mac . Write ( [ ] byte ( fmt . Sprint ( taskID ) ) )
mac . Write ( [ ] byte ( fmt . Sprint ( artifactID ) ) )
return mac . Sum ( nil )
}
func ( r artifactV4Routes ) buildArtifactURL ( ctx * ArtifactContext , endp , artifactName string , taskID int64 ) string {
func ( r artifactV4Routes ) buildArtifactURL ( ctx * ArtifactContext , endp , artifactName string , taskID , artifactID int64 ) string {
expires := time . Now ( ) . Add ( 60 * time . Minute ) . Format ( "2006-01-02 15:04:05.999999999 -0700 MST" )
uploadURL := strings . TrimSuffix ( httplib . GuessCurrentAppURL ( ctx ) , "/" ) + strings . TrimSuffix ( r . prefix , "/" ) +
"/" + endp + "?sig=" + base64 . URLEncoding . EncodeToString ( r . buildSignature ( endp , expires , artifactName , taskID ) ) + "&expires=" + url . QueryEscape ( expires ) + "&artifactName=" + url . QueryEscape ( artifactName ) + "&taskID=" + fmt . Sprint ( taskID )
"/" + endp + "?sig=" + base64 . URLEncoding . EncodeToString ( r . buildSignature ( endp , expires , artifactName , taskID , artifactID ) ) + "&expires=" + url . QueryEscape ( expires ) + "&artifactName=" + url . QueryEscape ( artifactName ) + "&taskID=" + fmt . Sprint ( taskID ) + "&artifactID=" + fmt . Sprint ( artifact ID )
return uploadURL
}
func ( r artifactV4Routes ) verifySignature ( ctx * ArtifactContext , endp string ) ( * actions . ActionTask , string , bool ) {
rawTaskID := ctx . Req . URL . Query ( ) . Get ( "taskID" )
rawArtifactID := ctx . Req . URL . Query ( ) . Get ( "artifactID" )
sig := ctx . Req . URL . Query ( ) . Get ( "sig" )
expires := ctx . Req . URL . Query ( ) . Get ( "expires" )
artifactName := ctx . Req . URL . Query ( ) . Get ( "artifactName" )
dsig , _ := base64 . URLEncoding . DecodeString ( sig )
taskID , _ := strconv . ParseInt ( rawTaskID , 10 , 64 )
artifactID , _ := strconv . ParseInt ( rawArtifactID , 10 , 64 )
expecedsig := r . buildSignature ( endp , expires , artifactName , taskID )
expecedsig := r . buildSignature ( endp , expires , artifactName , taskID , artifactID )
if ! hmac . Equal ( dsig , expecedsig ) {
log . Error ( "Error unauthorized" )
ctx . Error ( http . StatusUnauthorized , "Error unauthorized" )
@ -271,6 +282,8 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
return
}
artifact . ContentEncoding = ArtifactV4ContentEncoding
artifact . FileSize = 0
artifact . FileCompressedSize = 0
if err := actions . UpdateArtifactByID ( ctx , artifact . ID , artifact ) ; err != nil {
log . Error ( "Error UpdateArtifactByID: %v" , err )
ctx . Error ( http . StatusInternalServerError , "Error UpdateArtifactByID" )
@ -279,7 +292,7 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
respData := CreateArtifactResponse {
Ok : true ,
SignedUploadUrl : r . buildArtifactURL ( ctx , "UploadArtifact" , artifactName , ctx . ActionTask . ID ) ,
SignedUploadUrl : r . buildArtifactURL ( ctx , "UploadArtifact" , artifactName , ctx . ActionTask . ID , artifact . ID ) ,
}
r . sendProtbufBody ( ctx , & respData )
}
@ -293,6 +306,8 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
comp := ctx . Req . URL . Query ( ) . Get ( "comp" )
switch comp {
case "block" , "appendBlock" :
blockid := ctx . Req . URL . Query ( ) . Get ( "blockid" )
if blockid == "" {
// get artifact by name
artifact , err := r . getArtifactByName ( ctx , task . Job . RunID , artifactName )
if err != nil {
@ -301,11 +316,6 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
return
}
if comp == "block" {
artifact . FileSize = 0
artifact . FileCompressedSize = 0
}
_ , err = appendUploadChunk ( r . fs , ctx , artifact , artifact . FileSize , ctx . Req . ContentLength , artifact . RunID )
if err != nil {
log . Error ( "Error runner api getting task: task is not running" )
@ -319,12 +329,54 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
ctx . Error ( http . StatusInternalServerError , "Error UpdateArtifactByID" )
return
}
} else {
_ , err := r . fs . Save ( fmt . Sprintf ( "tmpv4%d/block-%d-%d-%s" , task . Job . RunID , task . Job . RunID , ctx . Req . ContentLength , base64 . URLEncoding . EncodeToString ( [ ] byte ( blockid ) ) ) , ctx . Req . Body , - 1 )
if err != nil {
log . Error ( "Error runner api getting task: task is not running" )
ctx . Error ( http . StatusInternalServerError , "Error runner api getting task: task is not running" )
return
}
}
ctx . JSON ( http . StatusCreated , "appended" )
case "blocklist" :
rawArtifactID := ctx . Req . URL . Query ( ) . Get ( "artifactID" )
artifactID , _ := strconv . ParseInt ( rawArtifactID , 10 , 64 )
_ , err := r . fs . Save ( fmt . Sprintf ( "tmpv4%d/%d-%d-blocklist" , task . Job . RunID , task . Job . RunID , artifactID ) , ctx . Req . Body , - 1 )
if err != nil {
log . Error ( "Error runner api getting task: task is not running" )
ctx . Error ( http . StatusInternalServerError , "Error runner api getting task: task is not running" )
return
}
ctx . JSON ( http . StatusCreated , "created" )
}
}
type BlockList struct {
Latest [ ] string ` xml:"Latest" `
}
type Latest struct {
Value string ` xml:",chardata" `
}
func ( r * artifactV4Routes ) readBlockList ( runID , artifactID int64 ) ( * BlockList , error ) {
blockListName := fmt . Sprintf ( "tmpv4%d/%d-%d-blocklist" , runID , runID , artifactID )
s , err := r . fs . Open ( blockListName )
if err != nil {
return nil , err
}
xdec := xml . NewDecoder ( s )
blockList := & BlockList { }
err = xdec . Decode ( blockList )
delerr := r . fs . Delete ( blockListName )
if delerr != nil {
log . Warn ( "Failed to delete blockList %s: %v" , blockListName , delerr )
}
return blockList , err
}
func ( r * artifactV4Routes ) finalizeArtifact ( ctx * ArtifactContext ) {
var req FinalizeArtifactRequest
@ -343,18 +395,34 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) {
ctx . Error ( http . StatusNotFound , "Error artifact not found" )
return
}
var chunks [ ] * chunkFileItem
blockList , err := r . readBlockList ( runID , artifact . ID )
if err != nil {
log . Warn ( "Failed to read BlockList, fallback to old behavior: %v" , err )
chunkMap , err := listChunksByRunID ( r . fs , runID )
if err != nil {
log . Error ( "Error merge chunks: %v" , err )
ctx . Error ( http . StatusInternalServerError , "Error merge chunks" )
return
}
chunks , ok : = chunkMap [ artifact . ID ]
chunks , ok = chunkMap [ artifact . ID ]
if ! ok {
log . Error ( "Error merge chunks" )
ctx . Error ( http . StatusInternalServerError , "Error merge chunks" )
return
}
} else {
chunks , err = listChunksByRunIDV4 ( r . fs , runID , artifact . ID , blockList )
if err != nil {
log . Error ( "Error merge chunks: %v" , err )
ctx . Error ( http . StatusInternalServerError , "Error merge chunks" )
return
}
artifact . FileSize = chunks [ len ( chunks ) - 1 ] . End + 1
artifact . FileCompressedSize = chunks [ len ( chunks ) - 1 ] . End + 1
}
checksum := ""
if req . Hash != nil {
checksum = req . Hash . Value
@ -455,7 +523,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
}
}
if respData . SignedUrl == "" {
respData . SignedUrl = r . buildArtifactURL ( ctx , "DownloadArtifact" , artifactName , ctx . ActionTask . ID )
respData . SignedUrl = r . buildArtifactURL ( ctx , "DownloadArtifact" , artifactName , ctx . ActionTask . ID , artifact . ID )
}
r . sendProtbufBody ( ctx , & respData )
}