2018-10-19 20:31:58 +00:00
|
|
|
package level
|
|
|
|
|
2021-06-07 01:59:04 +00:00
|
|
|
import (
|
2022-04-30 19:47:35 +00:00
|
|
|
"archive/zip"
|
|
|
|
"encoding/json"
|
2021-06-07 01:59:04 +00:00
|
|
|
"errors"
|
2022-04-30 19:47:35 +00:00
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2021-06-07 01:59:04 +00:00
|
|
|
"sort"
|
2021-06-13 21:53:21 +00:00
|
|
|
"strings"
|
2022-04-30 19:47:35 +00:00
|
|
|
|
|
|
|
"git.kirsle.net/apps/doodle/pkg/log"
|
2021-06-07 01:59:04 +00:00
|
|
|
)
|
|
|
|
|
2022-04-30 19:47:35 +00:00
|
|
|
/*
|
|
|
|
FileSystem embeds a map of files inside a parent drawing.
|
|
|
|
|
|
|
|
Old-style drawings this was a map of filenames to their data in the JSON.
|
|
|
|
New-style drawings this just holds the filenames and the data is read
|
|
|
|
from the zipfile on demand.
|
|
|
|
*/
|
|
|
|
type FileSystem struct {
|
|
|
|
filemap map[string]File `json:"-"` // Legacy JSON format
|
|
|
|
Zipfile *zip.Reader `json:"-"` // New Zipfile format accessor
|
|
|
|
}
|
2018-10-19 20:31:58 +00:00
|
|
|
|
|
|
|
// File holds details about a file in the FileSystem.
|
|
|
|
type File struct {
|
2022-04-30 19:47:35 +00:00
|
|
|
Data []byte `json:"data,omitempty"`
|
2018-10-19 20:31:58 +00:00
|
|
|
}
|
2021-06-07 01:59:04 +00:00
|
|
|
|
2022-04-30 19:47:35 +00:00
|
|
|
// NewFileSystem initializes the FileSystem struct.
|
|
|
|
func NewFileSystem() *FileSystem {
|
|
|
|
return &FileSystem{
|
|
|
|
filemap: map[string]File{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get a file from the FileSystem.
|
|
|
|
func (fs *FileSystem) Get(filename string) ([]byte, error) {
|
|
|
|
if fs.filemap == nil {
|
|
|
|
fs.filemap = map[string]File{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Legacy file map.
|
|
|
|
if file, ok := fs.filemap[filename]; ok {
|
|
|
|
if len(file.Data) > 0 {
|
|
|
|
return file.Data, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check in the zipfile.
|
|
|
|
if fs.Zipfile != nil {
|
|
|
|
file, err := fs.Zipfile.Open(filename)
|
|
|
|
if err != nil {
|
|
|
|
return []byte{}, fmt.Errorf("%s: not in zipfile: %s", filename, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
bin, err := ioutil.ReadAll(file)
|
|
|
|
if err != nil {
|
|
|
|
return bin, fmt.Errorf("%s: couldn't read zipfile member: %s", filename, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return bin, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return []byte{}, fmt.Errorf("no such file")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set a file into the FileSystem. Note: it will go into the legacy map
|
|
|
|
// structure until the next save to disk, at which point queued files
|
|
|
|
// are committed to ZIP.
|
|
|
|
func (fs *FileSystem) Set(filename string, data []byte) {
|
|
|
|
if fs.filemap == nil {
|
|
|
|
fs.filemap = map[string]File{}
|
2021-06-07 01:59:04 +00:00
|
|
|
}
|
|
|
|
|
2022-04-30 19:47:35 +00:00
|
|
|
fs.filemap[filename] = File{
|
2021-06-07 01:59:04 +00:00
|
|
|
Data: data,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-30 19:47:35 +00:00
|
|
|
// Delete a file from the FileSystem. This will store zero bytes in the
|
|
|
|
// legacy file map structure to mark it for deletion. On the next save,
|
|
|
|
// filemap files with zero bytes skip the ZIP archive.
|
|
|
|
func (fs *FileSystem) Delete(filename string) {
|
|
|
|
if fs.filemap == nil {
|
|
|
|
fs.filemap = map[string]File{}
|
2021-06-07 01:59:04 +00:00
|
|
|
}
|
|
|
|
|
2022-04-30 19:47:35 +00:00
|
|
|
fs.filemap[filename] = File{
|
|
|
|
Data: []byte{},
|
2021-06-07 01:59:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-30 19:47:35 +00:00
|
|
|
// List files in the FileSystem, including the ZIP file.
|
|
|
|
//
|
|
|
|
// In the ZIP file, attachments are under the "assets/" prefix so this
|
|
|
|
// function won't mistakenly return chunks or level.json/doodad.json.
|
|
|
|
func (fs *FileSystem) List() []string {
|
|
|
|
var (
|
|
|
|
result = []string{}
|
|
|
|
seen = map[string]interface{}{}
|
|
|
|
)
|
|
|
|
|
|
|
|
// List the legacy or recently modified files first.
|
|
|
|
if fs.filemap != nil {
|
|
|
|
for filename := range fs.filemap {
|
|
|
|
result = append(result, filename)
|
|
|
|
seen[filename] = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// List the zipfile members.
|
|
|
|
if fs.Zipfile != nil {
|
|
|
|
for _, file := range fs.Zipfile.File {
|
|
|
|
if !strings.HasPrefix(file.Name, "assets/") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, ok := seen[file.Name]; !ok {
|
|
|
|
result = append(result, file.Name)
|
|
|
|
seen[file.Name] = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Strings(result)
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListPrefix returns a list of files starting with the prefix.
|
|
|
|
func (fs *FileSystem) ListPrefix(prefix string) []string {
|
|
|
|
var result = []string{}
|
|
|
|
for _, name := range fs.List() {
|
|
|
|
if strings.HasPrefix(name, prefix) {
|
|
|
|
result = append(result, name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalJSON reads in a FileSystem from its legacy JSON representation.
|
|
|
|
func (fs *FileSystem) UnmarshalJSON(text []byte) error {
|
|
|
|
// Legacy format was a simple map[string]File.
|
|
|
|
var legacy map[string]File
|
|
|
|
err := json.Unmarshal(text, &legacy)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fs.filemap = legacy
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// MigrateZipfile is called on save to write attached files to the ZIP
|
|
|
|
// file format.
|
|
|
|
func (fs *FileSystem) MigrateZipfile(zf *zip.Writer) error {
|
|
|
|
// Identify the files that we have marked for deletion.
|
|
|
|
var (
|
|
|
|
filesDeleted = map[string]interface{}{}
|
|
|
|
filesZipped = map[string]interface{}{}
|
|
|
|
)
|
|
|
|
if fs.filemap != nil {
|
|
|
|
for filename, data := range fs.filemap {
|
|
|
|
if len(data.Data) == 0 {
|
|
|
|
log.Info("FileSystem.MigrateZipfile: %s has become empty, remove from zip", filename)
|
|
|
|
filesDeleted[filename] = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy all COLD STORED files from the old Zipfile into the new Zipfile
|
|
|
|
// except for the ones marked for deletion OR the ones currently in the
|
|
|
|
// warm cache which will be written next.
|
|
|
|
if fs.Zipfile != nil {
|
|
|
|
log.Info("FileSystem.MigrateZipfile: Copying files from old zip to new zip")
|
|
|
|
for _, file := range fs.Zipfile.File {
|
|
|
|
if !strings.HasPrefix(file.Name, "assets/") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, ok := filesDeleted[file.Name]; ok {
|
|
|
|
log.Debug("Skip copying attachment %s: was marked for deletion")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Skip files currently in memory.
|
|
|
|
if fs.filemap != nil {
|
|
|
|
if _, ok := fs.filemap[file.Name]; ok {
|
|
|
|
log.Debug("Skip copying attachment %s: one is loaded in memory")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debug("Copy zipfile attachment %s", file.Name)
|
|
|
|
filesZipped[file.Name] = nil
|
|
|
|
|
|
|
|
if err := zf.Copy(file); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2021-06-07 01:59:04 +00:00
|
|
|
}
|
|
|
|
|
2022-04-30 19:47:35 +00:00
|
|
|
// Export currently warmed up files to ZIP, these will be ones that
|
|
|
|
// were updated recently OR legacy files from an old level read.
|
|
|
|
if fs.filemap != nil {
|
|
|
|
log.Info("FileSystem.MigrateZipfile: has %d files in memory to write to ZIP", len(fs.filemap))
|
|
|
|
for filename, file := range fs.filemap {
|
|
|
|
if _, ok := filesZipped[filename]; ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
writer, err := zf.Create(filename)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
n, err := writer.Write(file.Data)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debug("Exported file to zip: %s (%d bytes)", filename, n)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////
|
|
|
|
// Level class methods for its filesystem access
|
|
|
|
////////////////
|
|
|
|
|
|
|
|
// SetFile sets a file's data in the level.
|
|
|
|
func (l *Level) SetFile(filename string, data []byte) {
|
|
|
|
l.Files.Set(filename, data)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetFile looks up an embedded file.
|
|
|
|
func (l *Level) GetFile(filename string) ([]byte, error) {
|
|
|
|
if l.Files == nil {
|
|
|
|
return []byte{}, errors.New("filesystem not initialized")
|
2021-06-07 01:59:04 +00:00
|
|
|
}
|
2022-04-30 19:47:35 +00:00
|
|
|
return l.Files.Get(filename)
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteFile removes an embedded file.
|
|
|
|
func (l *Level) DeleteFile(filename string) bool {
|
|
|
|
l.Files.Delete(filename)
|
|
|
|
return true
|
2021-06-07 01:59:04 +00:00
|
|
|
}
|
|
|
|
|
2022-01-18 02:51:11 +00:00
|
|
|
// DeleteFiles removes all files beginning with the prefix.
|
|
|
|
func (l *Level) DeleteFiles(prefix string) int {
|
|
|
|
var count int
|
2022-04-30 19:47:35 +00:00
|
|
|
for _, filename := range l.Files.ListPrefix(prefix) {
|
|
|
|
l.Files.Delete(filename)
|
|
|
|
count++
|
2022-01-18 02:51:11 +00:00
|
|
|
}
|
|
|
|
return count
|
|
|
|
}
|
|
|
|
|
2021-06-07 01:59:04 +00:00
|
|
|
// ListFiles returns the list of all embedded file names, alphabetically.
|
|
|
|
func (l *Level) ListFiles() []string {
|
2022-04-30 19:47:35 +00:00
|
|
|
if l == nil {
|
|
|
|
log.Error("Level.ListFiles() was called on a nil Level??")
|
|
|
|
return []string{}
|
2021-06-07 01:59:04 +00:00
|
|
|
}
|
|
|
|
|
2022-04-30 19:47:35 +00:00
|
|
|
if l.Files == nil {
|
|
|
|
log.Error("Level(%s).ListFiles: FileSystem not initialized", l.Title)
|
|
|
|
return []string{}
|
2021-06-07 01:59:04 +00:00
|
|
|
}
|
2022-04-30 19:47:35 +00:00
|
|
|
return l.Files.List()
|
2021-06-13 21:53:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ListFilesAt returns the list of files having a common prefix.
|
|
|
|
func (l *Level) ListFilesAt(prefix string) []string {
|
2022-04-30 19:47:35 +00:00
|
|
|
return l.Files.ListPrefix(prefix)
|
2021-06-13 21:53:21 +00:00
|
|
|
}
|