2017-03-05 17:26:04 +00:00
package main
import (
2017-03-05 23:17:35 +00:00
"archive/tar"
2017-03-05 17:26:04 +00:00
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
2017-03-05 22:19:43 +00:00
"net/url"
2017-03-05 17:26:04 +00:00
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"time"
2017-03-05 22:49:23 +00:00
"github.com/Luzifer/go_helpers/str"
2017-03-05 17:26:04 +00:00
"github.com/Luzifer/rconfig"
log "github.com/Sirupsen/logrus"
"github.com/gorilla/mux"
uuid "github.com/satori/go.uuid"
)
var (
cfg = struct {
2017-03-05 22:19:43 +00:00
ExecutionScript string ` flag:"script" default:"/go/src/github.com/Luzifer/tex-api/tex-build.sh" description:"Script to execute (needs to generate output directory)" `
2017-03-05 17:26:04 +00:00
Listen string ` flag:"listen" default:":3000" description:"IP/Port to listen on" `
StorageDir string ` flag:"storage-dir" default:"/storage" description:"Where to store uploaded ZIPs and resulting files" `
VersionAndExit bool ` flag:"version" default:"false" description:"Prints current version and exits" `
} { }
version = "dev"
router = mux . NewRouter ( )
)
type status string
const (
statusCreated = "created"
statusStarted = "started"
statusError = "error"
statusFinished = "finished"
)
const (
filenameInput = "input.zip"
filenameStatus = "status.json"
filenameOutputDir = "output"
sleepBase = 1.5
)
2017-03-06 08:10:36 +00:00
type jobStatus struct {
2017-03-05 17:26:04 +00:00
UUID string ` json:"uuid" `
CreatedAt time . Time ` json:"created_at" `
UpdatedAt time . Time ` json:"updated_at" `
Status status ` json:"status" `
}
2017-03-06 08:10:36 +00:00
func loadStatusByUUID ( uid uuid . UUID ) ( * jobStatus , error ) {
2017-03-05 17:26:04 +00:00
statusFile := pathFromUUID ( uid , filenameStatus )
2017-03-06 08:10:36 +00:00
status := jobStatus { }
2017-03-05 17:26:04 +00:00
if f , err := os . Open ( statusFile ) ; err == nil {
defer f . Close ( )
if err = json . NewDecoder ( f ) . Decode ( & status ) ; err != nil {
return nil , err
}
} else {
return nil , err
}
return & status , nil
}
2017-03-06 08:10:36 +00:00
func ( s * jobStatus ) UpdateStatus ( st status ) {
2017-03-05 17:26:04 +00:00
s . Status = st
s . UpdatedAt = time . Now ( )
}
2017-03-06 08:10:36 +00:00
func ( s jobStatus ) Save ( ) error {
2017-03-05 17:26:04 +00:00
uid , _ := uuid . FromString ( s . UUID )
f , err := os . Create ( pathFromUUID ( uid , filenameStatus ) )
if err != nil {
return err
}
defer f . Close ( )
return json . NewEncoder ( f ) . Encode ( s )
}
2017-03-05 22:19:43 +00:00
func urlMust ( u * url . URL , err error ) * url . URL {
if err != nil {
log . Fatalf ( "Unable to retrieve URL from router: %s" , err )
}
return u
}
2017-03-05 17:26:04 +00:00
func init ( ) {
if err := rconfig . Parse ( & cfg ) ; err != nil {
log . Fatalf ( "Unable to parse commandline options: %s" , err )
}
if cfg . VersionAndExit {
fmt . Printf ( "tex-api %s\n" , version )
os . Exit ( 0 )
}
}
func main ( ) {
router . HandleFunc ( "/" , apiDocs ) . Methods ( "GET" ) . Name ( "apiDocs" )
router . HandleFunc ( "/job" , startNewJob ) . Methods ( "POST" ) . Name ( "startNewJob" )
router . HandleFunc ( "/job/{uid:[0-9a-z-]{36}}" , getJobStatus ) . Methods ( "GET" ) . Name ( "getJobStatus" )
router . HandleFunc ( "/job/{uid:[0-9a-z-]{36}}/wait" , waitForJob ) . Methods ( "GET" ) . Name ( "waitForJob" )
router . HandleFunc ( "/job/{uid:[0-9a-z-]{36}}/download" , downloadAssets ) . Methods ( "GET" ) . Name ( "downloadAssets" )
log . Fatalf ( "%s" , http . ListenAndServe ( cfg . Listen , router ) )
}
func pathFromUUID ( uid uuid . UUID , filename string ) string {
return path . Join ( cfg . StorageDir , uid . String ( ) , filename )
}
func apiDocs ( res http . ResponseWriter , r * http . Request ) {
http . Error ( res , "Not implemented yet" , http . StatusInternalServerError )
}
func startNewJob ( res http . ResponseWriter , r * http . Request ) {
jobUUID := uuid . NewV4 ( )
inputFile := pathFromUUID ( jobUUID , filenameInput )
statusFile := pathFromUUID ( jobUUID , filenameStatus )
if err := os . Mkdir ( path . Dir ( inputFile ) , 0755 ) ; err != nil {
log . Errorf ( "Unable to create job dir %q: %s" , path . Dir ( inputFile ) , err )
}
if f , err := os . Create ( inputFile ) ; err == nil {
2017-03-05 22:19:43 +00:00
defer f . Close ( )
2017-03-06 08:10:36 +00:00
if _ , copyErr := io . Copy ( f , r . Body ) ; err != nil {
log . Errorf ( "Unable to copy input file %q: %s" , inputFile , copyErr )
2017-03-05 22:19:43 +00:00
http . Error ( res , "An error ocurred. See details in log." , http . StatusInternalServerError )
return
}
f . Sync ( )
2017-03-05 17:26:04 +00:00
} else {
log . Errorf ( "Unable to write input file %q: %s" , inputFile , err )
http . Error ( res , "An error ocurred. See details in log." , http . StatusInternalServerError )
return
}
2017-03-06 08:10:36 +00:00
status := jobStatus {
2017-03-05 17:26:04 +00:00
UUID : jobUUID . String ( ) ,
CreatedAt : time . Now ( ) ,
UpdatedAt : time . Now ( ) ,
Status : statusCreated ,
}
if err := status . Save ( ) ; err != nil {
log . Errorf ( "Unable to create status file %q: %s" , statusFile , err )
http . Error ( res , "An error ocurred. See details in log." , http . StatusInternalServerError )
return
}
go jobProcessor ( jobUUID )
2017-03-05 22:19:43 +00:00
u := urlMust ( router . Get ( "waitForJob" ) . URL ( "uid" , jobUUID . String ( ) ) )
2017-03-05 17:26:04 +00:00
http . Redirect ( res , r , u . String ( ) , http . StatusFound )
}
func getJobStatus ( res http . ResponseWriter , r * http . Request ) {
vars := mux . Vars ( r )
uid , err := uuid . FromString ( vars [ "uid" ] )
if err != nil {
http . Error ( res , "UUID had unexpected format!" , http . StatusBadRequest )
return
}
if status , err := loadStatusByUUID ( uid ) ; err == nil {
2017-03-06 08:10:36 +00:00
if encErr := json . NewEncoder ( res ) . Encode ( status ) ; err != nil {
log . Errorf ( "Unable to serialize status file: %s" , encErr )
2017-03-05 17:26:04 +00:00
http . Error ( res , "An error ocurred. See details in log." , http . StatusInternalServerError )
return
}
} else {
log . Errorf ( "Unable to read status file: %s" , err )
http . Error ( res , "An error ocurred. See details in log." , http . StatusInternalServerError )
return
}
}
func waitForJob ( res http . ResponseWriter , r * http . Request ) {
vars := mux . Vars ( r )
uid , err := uuid . FromString ( vars [ "uid" ] )
if err != nil {
http . Error ( res , "UUID had unexpected format!" , http . StatusBadRequest )
return
}
var loop int
if v := r . URL . Query ( ) . Get ( "loop" ) ; v != "" {
2017-03-06 08:10:36 +00:00
if pv , convErr := strconv . Atoi ( v ) ; convErr == nil {
2017-03-05 17:26:04 +00:00
loop = pv
}
}
loop ++
status , err := loadStatusByUUID ( uid )
if err != nil {
log . Errorf ( "Unable to read status file: %s" , err )
http . Error ( res , "An error ocurred. See details in log." , http . StatusInternalServerError )
return
}
2017-03-05 22:50:13 +00:00
switch status . Status {
case statusCreated :
fallthrough
case statusStarted :
2017-03-05 22:19:43 +00:00
u := urlMust ( router . Get ( "waitForJob" ) . URL ( "uid" , uid . String ( ) ) )
2017-03-05 17:26:04 +00:00
u . Query ( ) . Set ( "loop" , strconv . Itoa ( loop ) )
<- time . After ( time . Duration ( math . Pow ( sleepBase , float64 ( loop ) ) ) * time . Second )
http . Redirect ( res , r , u . String ( ) , http . StatusFound )
2017-03-05 22:35:59 +00:00
return
2017-03-05 17:26:04 +00:00
2017-03-05 22:50:13 +00:00
case statusError :
http . Error ( res , "Processing ran into an error." , http . StatusInternalServerError )
case statusFinished :
u := urlMust ( router . Get ( "downloadAssets" ) . URL ( "uid" , uid . String ( ) ) )
http . Redirect ( res , r , u . String ( ) , http . StatusFound )
}
2017-03-05 17:26:04 +00:00
}
2017-03-05 22:49:23 +00:00
func shouldPackFile ( extension string ) bool {
return str . StringInSlice ( extension , [ ] string {
".log" ,
".pdf" ,
} )
}
2017-03-05 17:26:04 +00:00
func buildAssetsZIP ( uid uuid . UUID ) ( io . Reader , error ) {
buf := new ( bytes . Buffer )
w := zip . NewWriter ( buf )
basePath := pathFromUUID ( uid , filenameOutputDir )
err := filepath . Walk ( basePath , func ( p string , info os . FileInfo , err error ) error {
if err != nil {
return err
}
2017-03-05 22:49:23 +00:00
if ! shouldPackFile ( path . Ext ( info . Name ( ) ) ) {
return nil
}
2017-03-05 17:26:04 +00:00
zipInfo , err := zip . FileInfoHeader ( info )
if err != nil {
return err
}
zipInfo . Name = strings . TrimLeft ( strings . Replace ( p , basePath , "" , 1 ) , "/\\" )
zipFile , err := w . CreateHeader ( zipInfo )
if err != nil {
return err
}
osFile , err := os . Open ( p )
if err != nil {
return err
}
io . Copy ( zipFile , osFile )
osFile . Close ( )
return nil
} )
if err != nil {
return nil , err
}
return buf , w . Close ( )
}
2017-03-05 23:17:35 +00:00
func buildAssetsTAR ( uid uuid . UUID ) ( io . Reader , error ) {
buf := new ( bytes . Buffer )
w := tar . NewWriter ( buf )
basePath := pathFromUUID ( uid , filenameOutputDir )
err := filepath . Walk ( basePath , func ( p string , info os . FileInfo , err error ) error {
if err != nil {
return err
}
if ! shouldPackFile ( path . Ext ( info . Name ( ) ) ) {
return nil
}
tarInfo , err := tar . FileInfoHeader ( info , "" )
if err != nil {
return err
}
tarInfo . Name = strings . TrimLeft ( strings . Replace ( p , basePath , "" , 1 ) , "/\\" )
err = w . WriteHeader ( tarInfo )
if err != nil {
return err
}
osFile , err := os . Open ( p )
if err != nil {
return err
}
io . Copy ( w , osFile )
osFile . Close ( )
return nil
} )
if err != nil {
return nil , err
}
return buf , w . Close ( )
}
2017-03-05 17:26:04 +00:00
func downloadAssets ( res http . ResponseWriter , r * http . Request ) {
vars := mux . Vars ( r )
uid , err := uuid . FromString ( vars [ "uid" ] )
if err != nil {
http . Error ( res , "UUID had unexpected format!" , http . StatusBadRequest )
return
}
var (
content io . Reader
filename string
)
switch r . Header . Get ( "Accept" ) {
2017-03-05 23:17:35 +00:00
case "application/tar" , "application/x-tar" , "applicaton/x-gtar" , "multipart/x-tar" , "application/x-compress" , "application/x-compressed" :
content , err = buildAssetsTAR ( uid )
filename = uid . String ( ) + ".tar"
2017-03-05 17:26:04 +00:00
default :
content , err = buildAssetsZIP ( uid )
filename = uid . String ( ) + ".zip"
}
if err != nil {
log . Errorf ( "Unable to generate downloadable asset: %s" , err )
http . Error ( res , "An error ocurred. See details in log." , http . StatusInternalServerError )
return
}
res . Header ( ) . Set ( "Content-Disposition" , fmt . Sprintf ( "attachment; filename=%q" , filename ) )
res . Header ( ) . Set ( "Content-Type" , "application/octet-stream" ) // TODO(luzifer): Use a correct type?
res . WriteHeader ( http . StatusOK )
io . Copy ( res , content )
}
func jobProcessor ( uid uuid . UUID ) {
processingDir := path . Dir ( pathFromUUID ( uid , filenameStatus ) )
status , err := loadStatusByUUID ( uid )
if err != nil {
log . Errorf ( "Unable to load status file in processing job: %s" , err )
return
}
cmd := exec . Command ( "/bin/bash" , cfg . ExecutionScript )
cmd . Dir = processingDir
2017-03-05 22:19:43 +00:00
cmd . Stderr = log . StandardLogger ( ) . WriterLevel ( log . ErrorLevel )
2017-03-05 17:26:04 +00:00
status . UpdateStatus ( statusStarted )
if err := status . Save ( ) ; err != nil {
2017-03-06 08:10:36 +00:00
log . Errorf ( "Unable to save status file" )
2017-03-05 17:26:04 +00:00
return
}
if err := cmd . Run ( ) ; err != nil {
status . UpdateStatus ( statusError )
if err := status . Save ( ) ; err != nil {
2017-03-06 08:10:36 +00:00
log . Errorf ( "Unable to save status file" )
2017-03-05 17:26:04 +00:00
return
}
return
}
status . UpdateStatus ( statusFinished )
if err := status . Save ( ) ; err != nil {
2017-03-06 08:10:36 +00:00
log . Errorf ( "Unable to save status file" )
2017-03-05 17:26:04 +00:00
return
}
}