github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/files.go (about)

     1  package common
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"crypto/sha256"
     7  	"encoding/base64"
     8  	"encoding/hex"
     9  	"errors"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"mime"
    13  	"net/http"
    14  	"net/url"
    15  	"os"
    16  	"path/filepath"
    17  	"strconv"
    18  	"strings"
    19  	"sync"
    20  
    21  	tmpl "github.com/Azareal/Gosora/tmpl_client"
    22  	"github.com/andybalholm/brotli"
    23  )
    24  
    25  //type SFileList map[string]*SFile
    26  //type SFileListShort map[string]*SFile
    27  
    28  var StaticFiles = SFileList{"/s/", make(map[string]*SFile), make(map[string]*SFile)}
    29  
    30  //var StaticFilesShort SFileList = make(map[string]*SFile)
    31  var staticFileMutex sync.RWMutex
    32  
    33  // ? Is it efficient to have two maps for this?
    34  type SFileList struct {
    35  	Prefix string
    36  	Long   map[string]*SFile
    37  	Short  map[string]*SFile
    38  }
    39  
    40  type SFile struct {
    41  	// TODO: Move these to the end?
    42  	Data     []byte
    43  	GzipData []byte
    44  	BrData   []byte
    45  
    46  	Sha256 string
    47  	Sha256I string
    48  	OName  string
    49  	Pos    int64
    50  
    51  	Length        int64
    52  	StrLength     string
    53  	GzipLength    int64
    54  	StrGzipLength string
    55  	BrLength      int64
    56  	StrBrLength   string
    57  
    58  	Mimetype         string
    59  	Info             os.FileInfo
    60  	FormattedModTime string
    61  }
    62  
    63  type CSSData struct {
    64  	Phrases map[string]string
    65  }
    66  
    67  func (l SFileList) JSTmplInit() error {
    68  	DebugLog("Initialising the client side templates")
    69  	return filepath.Walk("./tmpl_client", func(path string, f os.FileInfo, err error) error {
    70  		if f.IsDir() || strings.HasSuffix(path, "tmpl_list.go") || strings.HasSuffix(path, "stub.go") {
    71  			return nil
    72  		}
    73  		path = strings.Replace(path, "\\", "/", -1)
    74  		DebugLog("Processing client template " + path)
    75  		data, err := ioutil.ReadFile(path)
    76  		if err != nil {
    77  			return err
    78  		}
    79  
    80  		path = strings.TrimPrefix(path, "tmpl_client/")
    81  		tmplName := strings.TrimSuffix(path, ".jgo")
    82  		shortName := strings.TrimPrefix(tmplName, "tmpl_")
    83  
    84  		replace := func(data []byte, replaceThis, withThis string) []byte {
    85  			return bytes.Replace(data, []byte(replaceThis), []byte(withThis), -1)
    86  		}
    87  		rep := func(replaceThis, withThis string) {
    88  			data = replace(data, replaceThis, withThis)
    89  		}
    90  
    91  		startIndex, hasFunc := skipAllUntilCharsExist(data, 0, []byte("if(tmplInits===undefined)"))
    92  		if !hasFunc {
    93  			return errors.New("no init map found")
    94  		}
    95  		data = data[startIndex-len([]byte("if(tmplInits===undefined)")):]
    96  		rep("// nolint", "")
    97  		//rep("func ", "function ")
    98  		rep("func ", "function ")
    99  		rep(" error {\n", " {\nlet o=\"\"\n")
   100  		funcIndex, hasFunc := skipAllUntilCharsExist(data, 0, []byte("function Tmpl_"))
   101  		if !hasFunc {
   102  			return errors.New("no template function found")
   103  		}
   104  		spaceIndex, hasSpace := skipUntilIfExists(data, funcIndex, ' ')
   105  		if !hasSpace {
   106  			return errors.New("no spaces found after the template function name")
   107  		}
   108  		endBrace, hasBrace := skipUntilIfExists(data, spaceIndex, ')')
   109  		if !hasBrace {
   110  			return errors.New("no right brace found after the template function name")
   111  		}
   112  		fmt.Println("spaceIndex: ", spaceIndex)
   113  		fmt.Println("endBrace: ", endBrace)
   114  		fmt.Println("string(data[spaceIndex:endBrace]): ", string(data[spaceIndex:endBrace]))
   115  
   116  		preLen := len(data)
   117  		rep(string(data[spaceIndex:endBrace]), "")
   118  		rep("))\n", "  \n")
   119  		endBrace -= preLen - len(data) // Offset it as we've deleted portions
   120  		fmt.Println("new endBrace: ", endBrace)
   121  		fmt.Println("data: ", string(data))
   122  
   123  		/*showPos := func(data []byte, index int) (out string) {
   124  			out = "["
   125  			for j, char := range data {
   126  				if index == j {
   127  					out += "[" + string(char) + "] "
   128  				} else {
   129  					out += string(char) + " "
   130  				}
   131  			}
   132  			return out + "]"
   133  		}*/
   134  
   135  		// ? Can we just use a regex? I'm thinking of going more efficient, or just outright rolling wasm, this is a temp hack in a place where performance doesn't particularly matter
   136  		each := func(phrase string, h func(index int)) {
   137  			//fmt.Println("find each '" + phrase + "'")
   138  			index := endBrace
   139  			if index < 0 {
   140  				panic("index under zero: " + strconv.Itoa(index))
   141  			}
   142  			var foundIt bool
   143  			for {
   144  				//fmt.Println("in index: ", index)
   145  				//fmt.Println("pos: ", showPos(data, index))
   146  				index, foundIt = skipAllUntilCharsExist(data, index, []byte(phrase))
   147  				if !foundIt {
   148  					break
   149  				}
   150  				h(index)
   151  			}
   152  		}
   153  		each("strconv.Itoa(", func(index int) {
   154  			braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
   155  			if hasEndBrace {
   156  				data[braceAt] = ' ' // Blank it
   157  			}
   158  		})
   159  		each("[]byte(", func(index int) {
   160  			braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
   161  			if hasEndBrace {
   162  				data[braceAt] = ' ' // Blank it
   163  			}
   164  		})
   165  		each("StringToBytes(", func(index int) {
   166  			braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
   167  			if hasEndBrace {
   168  				data[braceAt] = ' ' // Blank it
   169  			}
   170  		})
   171  		each("w.Write(", func(index int) {
   172  			braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
   173  			if hasEndBrace {
   174  				data[braceAt] = ' ' // Blank it
   175  			}
   176  		})
   177  		each("RelativeTime(", func(index int) {
   178  			braceAt, _ := skipUntilIfExistsOrLine(data, index, 10)
   179  			if data[braceAt-1] == ' ' {
   180  				data[braceAt-1] = ' ' // Blank it
   181  			}
   182  		})
   183  		each("if ", func(index int) {
   184  			//fmt.Println("if index: ", index)
   185  			braceAt, hasBrace := skipUntilIfExistsOrLine(data, index, '{')
   186  			if hasBrace {
   187  				if data[braceAt-1] != ' ' {
   188  					panic("couldn't find space before brace, found ' " + string(data[braceAt-1]) + "' instead")
   189  				}
   190  				data[braceAt-1] = ')' // Drop a brace here to satisfy JS
   191  			}
   192  		})
   193  		each("for _, item := range ", func(index int) {
   194  			//fmt.Println("for index: ", index)
   195  			braceAt, hasBrace := skipUntilIfExists(data, index, '{')
   196  			if hasBrace {
   197  				if data[braceAt-1] != ' ' {
   198  					panic("couldn't find space before brace, found ' " + string(data[braceAt-1]) + "' instead")
   199  				}
   200  				data[braceAt-1] = ')' // Drop a brace here to satisfy JS
   201  			}
   202  		})
   203  		rep("for _, item := range ", "for(item of ")
   204  		rep("w.Write([]byte(", "o += ")
   205  		rep("w.Write(StringToBytes(", "o += ")
   206  		rep("w.Write(", "o += ")
   207  		rep("+= c.", "+= ")
   208  		rep("strconv.Itoa(", "")
   209  		rep("strconv.FormatInt(", "")
   210  		rep("	c.", "")
   211  		rep("phrases.", "")
   212  		rep(", 10;", "")
   213  
   214  		//rep("var plist = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "const plist = tmplPhrases[\""+tmplName+"\"];")
   215  		//rep("//var plist = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "const "+shortName+"_phrase_arr = tmplPhrases[\""+tmplName+"\"];")
   216  		rep("//var plist = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "const pl=tmplPhrases[\""+tmplName+"\"];")
   217  		rep(shortName+"_phrase_arr", "pl")
   218  		rep(shortName+"_phrase", "pl")
   219  		rep("tmpl_"+shortName+"_vars", "t_v")
   220  
   221  		rep("var c_v_", "let c_v_")
   222  		rep(`t_vars, ok := tmpl_i.`, `/*`)
   223  		rep("[]byte(", "")
   224  		rep("StringToBytes(", "")
   225  		rep("RelativeTime(t_v.", "t_v.Relative")
   226  		// TODO: Format dates properly on the client side
   227  		rep(".Format(\"2006-01-02 15:04:05\"", "")
   228  		rep(", 10", "")
   229  		rep("if ", "if(")
   230  		rep("return nil", "return o")
   231  		rep(" )", ")")
   232  		rep(" \n", "\n")
   233  		rep("\n", ";\n")
   234  		rep("{;", "{")
   235  		rep("};", "}")
   236  		rep("[;", "[")
   237  		rep(",;", ",")
   238  		rep("=;", "=")
   239  		rep(`,
   240  	});
   241  }`, "\n\t];")
   242  		rep(`=
   243  }`, "=[]")
   244  		rep("o += ", "o+=")
   245  		rep(shortName+"_frags[", "fr[")
   246  		rep("function Tmpl_"+shortName+"(t_v) {", "var Tmpl_"+shortName+"=(t_v)=>{")
   247  
   248  		fragset := tmpl.GetFrag(shortName)
   249  		if fragset != nil {
   250  			//sfrags := []byte("let " + shortName + "_frags=[\n")
   251  			sfrags := []byte("{const fr=[")
   252  			for i, frags := range fragset {
   253  				//sfrags = append(sfrags, []byte(shortName+"_frags.push(`"+string(frags)+"`);\n")...)
   254  				//sfrags = append(sfrags, []byte("`"+string(frags)+"`,\n")...)
   255  				if i == 0 {
   256  					sfrags = append(sfrags, []byte("`"+string(frags)+"`")...)
   257  				} else {
   258  					sfrags = append(sfrags, []byte(",`"+string(frags)+"`")...)
   259  				}
   260  			}
   261  			//sfrags = append(sfrags, []byte("];\n")...)
   262  			sfrags = append(sfrags, []byte("];")...)
   263  			data = append(sfrags, data...)
   264  		}
   265  		rep("\n;", "\n")
   266  		rep(";;", ";")
   267  
   268  		data = append(data, '}')
   269  		for name, _ := range Themes {
   270  			if strings.HasSuffix(shortName, "_"+name) {
   271  				data = append(data, "var Tmpl_"+strings.TrimSuffix(shortName, "_"+name)+"=Tmpl_"+shortName+";"...)
   272  				break
   273  			}
   274  		}
   275  
   276  		path = tmplName + ".js"
   277  		DebugLog("js path: ", path)
   278  		ext := filepath.Ext("/tmpl_client/" + path)
   279  
   280  		brData, err := CompressBytesBrotli(data)
   281  		if err != nil {
   282  			return err
   283  		}
   284  		// Don't use Brotli if we get meagre gains from it as it takes longer to process the responses
   285  		if len(brData) >= (len(data) + 110) {
   286  			brData = nil
   287  		} else {
   288  			diff := len(data) - len(brData)
   289  			if diff <= len(data)/100 {
   290  				brData = nil
   291  			}
   292  		}
   293  
   294  		gzipData, err := CompressBytesGzip(data)
   295  		if err != nil {
   296  			return err
   297  		}
   298  		// Don't use Gzip if we get meagre gains from it as it takes longer to process the responses
   299  		if len(gzipData) >= (len(data) + 120) {
   300  			gzipData = nil
   301  		} else {
   302  			diff := len(data) - len(gzipData)
   303  			if diff <= len(data)/100 {
   304  				gzipData = nil
   305  			}
   306  		}
   307  
   308  		// Get a checksum for CSPs and cache busting
   309  		hasher := sha256.New()
   310  		hasher.Write(data)
   311  		sum := hasher.Sum(nil)
   312  		checksum := hex.EncodeToString(sum)
   313  		integrity := base64.StdEncoding.EncodeToString(sum)
   314  
   315  		l.Set(l.Prefix+path, &SFile{data, gzipData, brData, checksum, integrity, l.Prefix + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
   316  
   317  		DebugLogf("Added the '%s' static file.", path)
   318  		return nil
   319  	})
   320  }
   321  
   322  func (l SFileList) Init() error {
   323  	return filepath.Walk("./public", func(path string, f os.FileInfo, err error) error {
   324  		if f.IsDir() {
   325  			return nil
   326  		}
   327  
   328  		path = strings.Replace(path, "\\", "/", -1)
   329  		data, err := ioutil.ReadFile(path)
   330  		if err != nil {
   331  			return err
   332  		}
   333  		path = strings.TrimPrefix(path, "public/")
   334  		ext := filepath.Ext("/public/" + path)
   335  		if ext == ".js" {
   336  			data = bytes.Replace(data, []byte("\r"), []byte(""), -1)
   337  		}
   338  		mimetype := mime.TypeByExtension(ext)
   339  
   340  		// Get a checksum for CSPs and cache busting
   341  		hasher := sha256.New()
   342  		hasher.Write(data)
   343  		sum := hasher.Sum(nil)
   344  		checksum := hex.EncodeToString(sum)
   345  		integrity := base64.StdEncoding.EncodeToString(sum)
   346  
   347  		// Avoid double-compressing images
   348  		var gzipData, brData []byte
   349  		if mimetype != "image/jpeg" && mimetype != "image/png" && mimetype != "image/gif" {
   350  			brData, err = CompressBytesBrotli(data)
   351  			if err != nil {
   352  				return err
   353  			}
   354  			// Don't use Brotli if we get meagre gains from it as it takes longer to process the responses
   355  			if len(brData) >= (len(data) + 130) {
   356  				brData = nil
   357  			} else {
   358  				diff := len(data) - len(brData)
   359  				if diff <= len(data)/100 {
   360  					brData = nil
   361  				}
   362  			}
   363  
   364  			gzipData, err = CompressBytesGzip(data)
   365  			if err != nil {
   366  				return err
   367  			}
   368  			// Don't use Gzip if we get meagre gains from it as it takes longer to process the responses
   369  			if len(gzipData) >= (len(data) + 150) {
   370  				gzipData = nil
   371  			} else {
   372  				diff := len(data) - len(gzipData)
   373  				if diff <= len(data)/100 {
   374  					gzipData = nil
   375  				}
   376  			}
   377  		}
   378  
   379  		l.Set(l.Prefix+path, &SFile{data, gzipData, brData, checksum, integrity, l.Prefix + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mimetype, f, f.ModTime().UTC().Format(http.TimeFormat)})
   380  
   381  		DebugLogf("Added the '%s' static file.", path)
   382  		return nil
   383  	})
   384  }
   385  
   386  func (l SFileList) Add(path, prefix string) error {
   387  	data, err := ioutil.ReadFile(path)
   388  	if err != nil {
   389  		return err
   390  	}
   391  	fi, err := os.Open(path)
   392  	if err != nil {
   393  		return err
   394  	}
   395  	f, err := fi.Stat()
   396  	if err != nil {
   397  		return err
   398  	}
   399  
   400  	ext := filepath.Ext(path)
   401  	path = strings.TrimPrefix(path, prefix)
   402  
   403  	brData, err := CompressBytesBrotli(data)
   404  	if err != nil {
   405  		return err
   406  	}
   407  	// Don't use Brotli if we get meagre gains from it as it takes longer to process the responses
   408  	if len(brData) >= (len(data) + 130) {
   409  		brData = nil
   410  	} else {
   411  		diff := len(data) - len(brData)
   412  		if diff <= len(data)/100 {
   413  			brData = nil
   414  		}
   415  	}
   416  
   417  	gzipData, err := CompressBytesGzip(data)
   418  	if err != nil {
   419  		return err
   420  	}
   421  	// Don't use Gzip if we get meagre gains from it as it takes longer to process the responses
   422  	if len(gzipData) >= (len(data) + 150) {
   423  		gzipData = nil
   424  	} else {
   425  		diff := len(data) - len(gzipData)
   426  		if diff <= len(data)/100 {
   427  			gzipData = nil
   428  		}
   429  	}
   430  
   431  	// Get a checksum for CSPs and cache busting
   432  	hasher := sha256.New()
   433  	hasher.Write(data)
   434  	sum := hasher.Sum(nil)
   435  		checksum := hex.EncodeToString(sum)
   436  		integrity := base64.StdEncoding.EncodeToString(sum)
   437  
   438  	l.Set(l.Prefix+path, &SFile{data, gzipData, brData, checksum, integrity, l.Prefix + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
   439  
   440  	DebugLogf("Added the '%s' static file", path)
   441  	return nil
   442  }
   443  
   444  func (l SFileList) Get(path string) (file *SFile, exists bool) {
   445  	staticFileMutex.RLock()
   446  	defer staticFileMutex.RUnlock()
   447  	file, exists = l.Long[path]
   448  	return file, exists
   449  }
   450  
   451  // fetch without /s/ to avoid allocing in pages.go
   452  func (l SFileList) GetShort(name string) (file *SFile, exists bool) {
   453  	staticFileMutex.RLock()
   454  	defer staticFileMutex.RUnlock()
   455  	file, exists = l.Short[name]
   456  	return file, exists
   457  }
   458  
   459  func (l SFileList) Set(name string, data *SFile) {
   460  	staticFileMutex.Lock()
   461  	defer staticFileMutex.Unlock()
   462  	// TODO: Propagate errors back up
   463  	uurl, err := url.Parse(name)
   464  	if err != nil {
   465  		return
   466  	}
   467  	l.Long[uurl.Path] = data
   468  	l.Short[strings.TrimPrefix(strings.TrimPrefix(name, l.Prefix), "/")] = data
   469  }
   470  
   471  var gzipBestCompress sync.Pool
   472  
   473  func CompressBytesGzip(in []byte) (b []byte, err error) {
   474  	var buf bytes.Buffer
   475  	ii := gzipBestCompress.Get()
   476  	var gz *gzip.Writer
   477  	if ii == nil {
   478  		gz, err = gzip.NewWriterLevel(&buf, gzip.BestCompression)
   479  		if err != nil {
   480  			return nil, err
   481  		}
   482  	} else {
   483  		gz = ii.(*gzip.Writer)
   484  		gz.Reset(&buf)
   485  	}
   486  	_, err = gz.Write(in)
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  	err = gz.Close()
   491  	if err != nil {
   492  		return nil, err
   493  	}
   494  	gzipBestCompress.Put(gz)
   495  	return buf.Bytes(), nil
   496  }
   497  
   498  func CompressBytesBrotli(in []byte) ([]byte, error) {
   499  	var buff bytes.Buffer
   500  	br := brotli.NewWriterLevel(&buff, brotli.BestCompression)
   501  	_, err := br.Write(in)
   502  	if err != nil {
   503  		return nil, err
   504  	}
   505  	err = br.Close()
   506  	if err != nil {
   507  		return nil, err
   508  	}
   509  	return buff.Bytes(), nil
   510  }