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 }