github.com/vulppine/fotoDen@v0.3.0/tool/theme.go (about) 1 package tool 2 3 import ( 4 "archive/zip" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "net/url" 11 "os" 12 "path" 13 "path/filepath" 14 "text/template" 15 16 "github.com/vulppine/fotoDen/generator" 17 ) 18 19 // Note: 20 // This new method of zipped themes should immediately 21 // replace the old method of unzipping an uninitialized 22 // theme into a folder. It should make things more 23 // easier, as rather than having to initialize a theme 24 // into a directory, all a user has to do in order 25 // to create a site is just throw a --theme flag into 26 // the command. Equally, a default theme should be 27 // embed during build in case the user doesn't have 28 // a custom theme.zip to use. 29 // 30 // The old method should be deprecated immediately. 31 32 // theme represents a struct containing an open 33 // zip archive for reading, plus a struct containing 34 // an internal theme.json file. It also caches some 35 // files for later use via the text/template system. 36 type theme struct { 37 a *zip.Reader 38 s struct { 39 ThemeName string 40 Stylesheets []string 41 Scripts []string 42 Other []string 43 } 44 f map[string]string // file cache 45 } 46 47 func zipFileReader(z string) (io.ReaderAt, int64, error) { 48 f, err := os.Open(z) 49 if checkError(err) { 50 return nil, 0, err 51 } 52 53 fi, err := f.Stat() 54 if checkError(err) { 55 return nil, 0, err 56 } 57 58 return f, fi.Size(), nil 59 } 60 61 func openTheme(z io.ReaderAt, zs int64) (*theme, error) { 62 var err error 63 64 t := new(theme) 65 66 t.a, err = zip.NewReader(z, zs) 67 if checkError(err) { 68 return nil, err 69 } 70 71 c, err := t.a.Open("theme.json") 72 if checkError(err) { 73 return nil, err 74 } 75 76 cb, err := io.ReadAll(c) 77 if checkError(err) { 78 return nil, err 79 } 80 81 json.Unmarshal(cb, &t.s) 82 83 verbose("theme config successfully opened") 84 85 t.f = make(map[string]string) 86 f := []string{ 87 "album-template.html", 88 "folder-template.html", 89 "photo-template.html", 90 "page-template.html", 91 } 92 93 for _, i := range f { 94 f, err := t.a.Open(filepath.Join("html", i)) 95 if err != nil { 96 verbose("could not open a required page template: " + i) 97 return nil, err 98 } 99 100 b, err := io.ReadAll(f) 101 if err != nil { 102 verbose("could not open a required page template: " + i) 103 return nil, err 104 } 105 106 t.f[i] = string(b) 107 } 108 109 return t, nil 110 } 111 112 // writeFile directly writes a file from a theme's zip.Reader 113 // to the given destination. 114 func (t *theme) writeFile(n string, d string) error { 115 verbose("attempting to write a file from zip: " + n) 116 f, err := t.a.Open(n) 117 if checkError(err) { 118 return err 119 } 120 fb, err := io.ReadAll(f) 121 if checkError(err) { 122 return err 123 } 124 125 g, err := os.Create(d) 126 if checkError(err) { 127 return err 128 } 129 130 _, err = g.Write(fb) 131 if checkError(err) { 132 return err 133 } 134 135 return nil 136 } 137 138 // writeDir writes the contents of a named directory in a 139 // theme's zip.Reader into the named directory d, creating 140 // a directory in the process. A list of files can be given 141 // to writeDir, allowing it to skip checking the entire 142 // directory. 143 // 144 // NOTE: I haven't implemented the directory read yet. 145 // This is expected to be used in conjunction with 146 // the theme.json setup! 147 func (t *theme) writeDir(n string, d string, files ...string) error { 148 verbose("attempting to write a directory from zip: " + n) 149 i, err := t.a.Open(n) 150 if checkError(err) { 151 return err 152 } 153 in, err := i.Stat() 154 if checkError(err) { 155 return err 156 } 157 158 if !in.IsDir() { 159 return errors.New("not a valid directory") 160 } 161 162 if len(files) == 0 { 163 return errors.New("zip file directory checking not implemented yet") 164 } 165 166 if !fileCheck(filepath.Join(d, n)) { 167 err = os.Mkdir(filepath.Join(d, n), 0755) 168 if checkError(err) { 169 return err 170 } 171 } 172 173 for _, f := range files { 174 err := t.writeFile(filepath.Join(n, f), filepath.Join(d, n, f)) 175 if checkError(err) { 176 return err 177 } 178 } 179 180 return nil 181 } 182 183 // copyTheme copies a theme to a destination string, 184 // making a directory within d containing the entire 185 // tree of the theme as described in theme.json, 186 // as well as copying over theme.json for later use. 187 func (t *theme) copyTheme(d string) error { 188 189 return nil 190 } 191 192 // webVars dictate where fotoDen gets its JavaScript and CSS files per page. 193 // Four variables are definite - and BaseURL is the most important one. 194 // PageVars indicate optional variables to be passed to the Go template engine. 195 type webVars struct { 196 BaseURL string 197 JSLocation string 198 CSSLocation string 199 IsStatic bool 200 PageVars map[string]string 201 } 202 203 // newWebVars creates a WebVars object. Takes a single URL string, and outputs 204 // a set of fotoDen compatible URLs. 205 func newWebVars(u, folder string) (*webVars, error) { 206 207 webvars := new(webVars) 208 url, err := url.Parse(u) 209 jsurl, err := url.Parse(u) 210 cssurl, err := url.Parse(u) 211 if err != nil { 212 return webvars, err 213 } 214 215 webvars.BaseURL = url.String() 216 if len(webvars.BaseURL) > 0 && webvars.BaseURL[len(webvars.BaseURL)-1] == '/' { 217 webvars.BaseURL = webvars.BaseURL[0 : len(webvars.BaseURL)-1] 218 } 219 220 f := new(generator.Folder) 221 fpath, _ := filepath.Abs(folder) 222 223 err = f.ReadFolderInfo(filepath.Join(fpath, "folderInfo.json")) 224 if err != nil { 225 return nil, err 226 } 227 228 superFolder, err := func() (string, error) { 229 f := new(generator.Folder) 230 231 _, err := os.Stat(filepath.Join(filepath.Dir(fpath), "folderInfo.json")) 232 if os.IsNotExist(err) { 233 return "", nil 234 } else if checkError(err) { 235 return "", err 236 } 237 238 verbose("Folder above is a fotoDen folder, using that...") 239 err = f.ReadFolderInfo(filepath.Join(filepath.Dir(fpath), "folderInfo.json")) 240 if checkError(err) { 241 return "", err 242 } 243 244 return f.Name, nil 245 }() 246 247 if checkError(err) { 248 verbose("Could not read folder above the current one") 249 } 250 251 jsurl.Path = path.Join(jsurl.Path, "js", "fotoDen.js") 252 webvars.JSLocation = jsurl.String() 253 254 cssurl.Path = path.Join(cssurl.Path, "css", "theme.css") 255 webvars.CSSLocation = cssurl.String() 256 257 if f.Static { 258 webvars.PageVars = map[string]string{ 259 "name": f.Name, 260 "desc": f.Desc, 261 "sfol": superFolder, 262 } 263 } 264 265 return webvars, nil 266 } 267 268 type page int 269 270 const ( 271 photo page = iota 272 album 273 folder 274 info 275 ) 276 277 // configurePage configures the various Go template 278 // variables within a page according to a specific type. 279 // u is the URL of a website, 280 // d is the destination that the result goes into, 281 // t is the type of page, 282 // i is the set of variables to use 283 func (t *theme) configurePage(u, d string, y page, i *webVars) error { 284 var f string 285 p := template.New("result") 286 287 r, err := os.Create(d) 288 if err != nil { 289 return err 290 } 291 292 switch y { 293 case photo: 294 f = t.f["photo-template.html"] 295 case album: 296 f = t.f["album-template.html"] 297 case folder: 298 f = t.f["folder-template.html"] 299 case info: 300 f = t.f["page-template.html"] 301 } 302 303 m, err := p.Parse(f) 304 if err != nil { 305 return err 306 } 307 308 return m.Execute(r, i) 309 } 310 311 // generateWeb takes a mode, a destinatination, and an optional map[string]string. 312 // If i is not nil, that map will be merged into the WebVars PageVars field. 313 func (t *theme) generateWeb(m, dest string, i map[string]string) error { 314 var err error 315 var v *webVars 316 317 if m == "folder" || m == "album" { 318 v, err = newWebVars(generator.CurrentConfig.WebBaseURL, dest) 319 if err != nil { 320 return err 321 } 322 } else { 323 v = new(webVars) 324 v.BaseURL = generator.CurrentConfig.WebBaseURL 325 v.PageVars = make(map[string]string) 326 } 327 328 if i != nil { 329 for k, a := range i { 330 v.PageVars[k] = a 331 } 332 } 333 334 switch m { 335 case "folder": 336 err = t.configurePage(generator.CurrentConfig.WebBaseURL, path.Join(dest, "index.html"), folder, v) 337 if checkError(err) { 338 return err 339 } 340 case "album": 341 err = t.configurePage(generator.CurrentConfig.WebBaseURL, path.Join(dest, "index.html"), album, v) 342 if checkError(err) { 343 return err 344 } 345 346 err = t.configurePage(generator.CurrentConfig.WebBaseURL, path.Join(dest, "photo.html"), photo, v) 347 if checkError(err) { 348 return err 349 } 350 case "page": 351 err = t.configurePage(generator.CurrentConfig.WebBaseURL, dest, info, v) 352 353 if checkError(err) { 354 return err 355 } 356 } 357 358 return nil 359 } 360 361 /// GLOBAL THEME VAR /// 362 // this probably *could* be mitigated by 363 // including the current theme's name in the 364 // config, allowing for functions to refer to 365 // the current *website*'s theme 366 367 var currentTheme *theme 368 369 func setCurrentTheme(t string) error { 370 c, err := os.UserConfigDir() 371 if checkError(err) { 372 return err 373 } 374 375 if t != "Default" { 376 p := filepath.Join(c, "fotoDen", "themes", t+".zip") 377 z, s, err := zipFileReader(p) 378 if checkError(err) { 379 return err 380 } 381 382 currentTheme, err = openTheme(z, s) 383 if checkError(err) { 384 return err 385 } 386 } else { 387 if isEmbed { 388 currentTheme, _ = openTheme(defaultThemeZipReader(), defaultThemeZipLen) 389 } else { 390 return fmt.Errorf("could not find a fotoDen theme to use") 391 } 392 } 393 394 return nil 395 } 396 397 func openDefaultTheme() error { 398 if fileCheck(path.Join(generator.RootConfigDir, "defaulttheme")) { 399 f, err := os.Open(path.Join(generator.RootConfigDir, "defaulttheme")) 400 d, err := ioutil.ReadAll(f) 401 if checkError(err) { 402 return err 403 } 404 405 err = setCurrentTheme(string(d)) 406 if checkError(err) { 407 return err 408 } 409 } else { 410 return fmt.Errorf("warning: could not find a default fotoDen theme to use") 411 } 412 413 return nil 414 } 415 416 // initTheme initializes a theme to a site, replacing 417 // all relevant variables according to what fotoDen 418 // needs in order to generate a gallery. 419 // u is the URL of the site 420 // e is the template directory of the site 421 // r is the root directory of the site 422 func (t *theme) initTheme(u string, e string, r string) error { 423 var err error 424 verbose("attempting to initialize a theme") 425 /*m, err := os.MkdirTemp("", "") 426 if checkError(err) { 427 return err 428 } 429 verbose("temp dir created in: " + m) 430 431 wvars, err := NewWebVars(u) 432 wvars.PageVars["pageContent"] = "{{.PageContent}}" // hacky 433 checkError(err) 434 435 hf := []string{"photo-template.html", "album-template.html", "folder-template.html", "page-template.html"} 436 437 verbose("generating temporary template setup files") 438 for _, f := range hf { 439 err = t.writeFile(filepath.Join("html", f), filepath.Join(m, f)) 440 if checkError(err) { 441 return err 442 } 443 } 444 445 verbose("writing HTML templates now to: " + e) 446 err = os.MkdirAll(filepath.Join(e, "html"), 0755) 447 if checkError(err) { 448 return err 449 } 450 for _, f := range hf { 451 err = ConfigureWebFile( 452 filepath.Join(m, f), 453 filepath.Join(e, "html", f), 454 wvars, 455 ) 456 if checkError(err) { 457 return err 458 } 459 }*/ 460 461 copyArray := func(f []string, n string) error { 462 if len(f) != 0 { 463 err = t.writeDir(n, r, f...) 464 if checkError(err) { 465 return err 466 } 467 } 468 469 return nil 470 } 471 472 verbose("copying over theme folders into: " + r) 473 err = copyArray(t.s.Stylesheets, "css") 474 err = copyArray(t.s.Scripts, "js") 475 err = copyArray(t.s.Other, "etc") 476 477 return err 478 }