1 package battlemap 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "os" 8 "path" 9 "strconv" 10 "strings" 11 "sync" 12 13 "vimagination.zapto.org/byteio" 14 "vimagination.zapto.org/keystore" 15 "vimagination.zapto.org/memio" 16 ) 17 18 type folder struct { 19 Folders map[string]*folder `json:"folders"` 20 Items map[string]uint64 `json:"items"` 21 } 22 23 func newFolder() *folder { 24 return &folder{ 25 Folders: make(map[string]*folder), 26 Items: make(map[string]uint64), 27 } 28 } 29 30 func (f *folder) WriteToX(lw *byteio.StickyLittleEndianWriter) { 31 lw.WriteUint64(uint64(len(f.Folders))) 32 for name, fd := range f.Folders { 33 lw.WriteStringX(name) 34 fd.WriteToX(lw) 35 } 36 lw.WriteUint64(uint64(len(f.Items))) 37 for name, iid := range f.Items { 38 lw.WriteStringX(name) 39 lw.WriteUint64(iid) 40 } 41 } 42 43 func (f *folder) ReadFromX(lr *byteio.StickyLittleEndianReader) { 44 fl := lr.ReadUint64() 45 f.Folders = make(map[string]*folder, fl) 46 for i := uint64(0); i < fl; i++ { 47 fd := newFolder() 48 name := lr.ReadStringX() 49 fd.ReadFromX(lr) 50 f.Folders[name] = fd 51 } 52 il := lr.ReadUint64() 53 f.Items = make(map[string]uint64, il) 54 for i := uint64(0); i < il; i++ { 55 name := lr.ReadStringX() 56 f.Items[name] = lr.ReadUint64() 57 } 58 } 59 60 type folders struct { 61 *Battlemap 62 *keystore.FileStore 63 fileType 64 65 mu sync.RWMutex 66 lastID uint64 67 root *folder 68 json memio.Buffer 69 } 70 71 func (f *folders) Init(b *Battlemap, store *keystore.FileStore, l linkManager) error { 72 f.Battlemap = b 73 f.FileStore = store 74 f.root = newFolder() 75 if err := f.Get(folderMetadata, f); err != nil && os.IsNotExist(err) { 76 return fmt.Errorf("error getting asset data: %w", err) 77 } 78 f.processFolder(f.root, l) 79 return f.encodeJSON() 80 } 81 82 func (f *folders) cleanup(l linkManager) { 83 for _, key := range f.Keys() { 84 id, err := strconv.ParseUint(key, 10, 64) 85 if err != nil { 86 continue 87 } 88 if _, ok := l[id]; !ok { 89 f.Remove(key) 90 } 91 } 92 } 93 94 func (f *folders) WriteTo(w io.Writer) (int64, error) { 95 lw := byteio.StickyLittleEndianWriter{Writer: w} 96 f.root.WriteToX(&lw) 97 return lw.Count, lw.Err 98 } 99 100 func (f *folders) ReadFrom(r io.Reader) (int64, error) { 101 lr := byteio.StickyLittleEndianReader{Reader: r} 102 f.root.ReadFromX(&lr) 103 return lr.Count, lr.Err 104 } 105 106 func addItemTo(items map[string]uint64, name string, id uint64) string { 107 return uniqueName(name, func(name string) bool { 108 if _, ok := items[name]; !ok { 109 items[name] = id 110 return true 111 } 112 return false 113 }) 114 } 115 116 func addFolderTo(folders map[string]*folder, name string, f *folder) string { 117 return uniqueName(name, func(name string) bool { 118 if _, ok := folders[name]; !ok { 119 folders[name] = f 120 return true 121 } 122 return false 123 }) 124 } 125 126 func (f *folders) processFolder(fd *folder, l linkManager) { 127 for _, g := range fd.Folders { 128 f.processFolder(g, l) 129 } 130 for name, is := range fd.Items { 131 if is == 0 || !f.Exists(strconv.FormatUint(is, 10)) { 132 delete(fd.Items, name) 133 } else { 134 l.setLink(is) 135 } 136 if is > f.lastID { 137 f.lastID = is 138 } 139 } 140 } 141 142 func (f *folders) getFolder(path string) *folder { 143 d := f.root 144 for _, p := range strings.Split(path, "/") { 145 if p == "" { 146 continue 147 } 148 e, ok := d.Folders[p] 149 if !ok { 150 return nil 151 } 152 d = e 153 } 154 return d 155 } 156 157 func splitAfterLastSlash(p string) (string, string) { 158 lastSlash := strings.LastIndexByte(p, '/') 159 if lastSlash >= 0 { 160 return p[:lastSlash], p[lastSlash+1:] 161 } 162 return "", p 163 } 164 165 func (f *folders) getParentFolder(p string) (parent *folder, name string, fd *folder) { 166 parentStr, name := splitAfterLastSlash(path.Clean(strings.TrimRight(p, "/"))) 167 if parentStr != "" { 168 parent = f.getFolder(parentStr) 169 if parent == nil { 170 return nil, "", nil 171 } 172 } else { 173 parent = f.root 174 } 175 fd = parent.Folders[name] 176 return parent, name, fd 177 } 178 179 func (f *folders) getFolderItem(p string) (parent *folder, name string, iid uint64) { 180 dir, file := path.Split(p) 181 parent = f.getFolder(path.Clean(dir)) 182 if parent == nil { 183 return nil, "", 0 184 } 185 iid = parent.Items[file] 186 return parent, file, iid 187 } 188 189 func (f *folders) saveFolders() { 190 f.Set(folderMetadata, f) 191 f.encodeJSON() 192 } 193 194 func (f *folders) encodeJSON() error { 195 f.json = memio.Buffer{} 196 return json.NewEncoder(&f.json).Encode(f.root) 197 } 198 199 func walkFolders(f *folder, fn func(map[string]uint64) bool) bool { 200 if fn(f.Items) { 201 return true 202 } 203 for _, f := range f.Folders { 204 if walkFolders(f, fn) { 205 return true 206 } 207 } 208 return false 209 } 210 211 func (f *folders) RPCData(cd ConnData, method string, data json.RawMessage) (interface{}, error) { 212 if cd.IsAdmin() { 213 switch method { 214 case "list": 215 return f.list(), nil 216 case "createFolder": 217 return f.folderCreate(cd, data) 218 case "move": 219 return f.itemMove(cd, data) 220 case "moveFolder": 221 return f.folderMove(cd, data) 222 case "remove": 223 return nil, f.itemDelete(cd, data) 224 case "removeFolder": 225 return nil, f.folderDelete(cd, data) 226 case "copy": 227 return f.copyItem(cd, data) 228 } 229 } 230 return nil, ErrUnknownMethod 231 } 232 233 func (f *folders) list() json.RawMessage { 234 f.mu.RLock() 235 data := f.json 236 f.mu.RUnlock() 237 return json.RawMessage(data) 238 } 239 240 func (f *folders) folderCreate(cd ConnData, data json.RawMessage) (string, error) { 241 var dir string 242 if err := json.Unmarshal(data, &dir); err != nil { 243 return "", err 244 } 245 f.mu.Lock() 246 defer f.mu.Unlock() 247 parent, name, _ := f.getParentFolder(dir) 248 if parent == nil || name == "" { 249 return "", ErrFolderNotFound 250 } 251 newName := addFolderTo(parent.Folders, name, newFolder()) 252 f.saveFolders() 253 dir = dir[:len(dir)-len(name)] + newName 254 f.socket.broadcastAdminChange(f.getBroadcastID(broadcastImageFolderAdd), data, cd.ID) 255 return dir, nil 256 } 257 258 type fromTo struct { 259 From string `json:"from"` 260 To string `json:"to"` 261 } 262 263 func (f *folders) itemMove(cd ConnData, data json.RawMessage) (string, error) { 264 var itemMove fromTo 265 if err := json.Unmarshal(data, &itemMove); err != nil { 266 return "", err 267 } 268 f.mu.Lock() 269 defer f.mu.Unlock() 270 oldParent, oldName, iid := f.getFolderItem(itemMove.From) 271 if oldParent == nil || iid == 0 { 272 return "", ErrItemNotFound 273 } 274 var ( 275 newParent *folder 276 newName string 277 ) 278 if strings.HasSuffix(itemMove.To, "/") { 279 newParent = f.getFolder(strings.TrimRight(itemMove.To, "/")) 280 newName = oldName 281 } else { 282 path, file := path.Split(itemMove.To) 283 newName = file 284 itemMove.To = strings.TrimRight(path, "/") 285 newParent = f.getFolder(itemMove.To) 286 } 287 delete(oldParent.Items, oldName) 288 newName = addItemTo(newParent.Items, newName, iid) 289 f.saveFolders() 290 itemMove.To += "/" + newName 291 f.socket.broadcastAdminChange(f.getBroadcastID(broadcastImageItemMove), data, cd.ID) 292 return itemMove.To, nil 293 } 294 295 func (f *folders) folderMove(cd ConnData, data json.RawMessage) (string, error) { 296 var folderMove fromTo 297 if err := json.Unmarshal(data, &folderMove); err != nil { 298 return "", err 299 } 300 f.mu.Lock() 301 defer f.mu.Unlock() 302 oldParent, oldName, fd := f.getParentFolder(folderMove.From) 303 if oldParent == nil || fd == nil { 304 return "", ErrFolderNotFound 305 } 306 var ( 307 newParent *folder 308 newName string 309 ) 310 if strings.HasSuffix(folderMove.To, "/") { 311 newParent = f.getFolder(strings.TrimRight(folderMove.To, "/")) 312 newName = oldName 313 } else { 314 path, file := path.Split(folderMove.To) 315 newName = file 316 folderMove.To = strings.TrimRight(path, "/") 317 newParent = f.getFolder(folderMove.To) 318 } 319 if strings.HasSuffix(folderMove.To, folderMove.From) { 320 return "", ErrCircularFolder 321 } 322 delete(oldParent.Folders, oldName) 323 newName = addFolderTo(newParent.Folders, newName, fd) 324 f.saveFolders() 325 folderMove.To += "/" + newName 326 f.socket.broadcastAdminChange(f.getBroadcastID(broadcastImageFolderMove), data, cd.ID) 327 return folderMove.To, nil 328 } 329 330 func (f *folders) itemDelete(cd ConnData, data json.RawMessage) error { 331 var item string 332 if err := json.Unmarshal(data, &item); err != nil { 333 return err 334 } 335 f.socket.broadcastAdminChange(f.getBroadcastID(broadcastImageItemRemove), data, cd.ID) 336 return f.itemDeleteString(item) 337 } 338 339 func (f *folders) itemDeleteString(item string) error { 340 f.mu.Lock() 341 defer f.mu.Unlock() 342 parent, oldName, iid := f.getFolderItem(item) 343 if parent == nil || iid == 0 { 344 return ErrItemNotFound 345 } 346 delete(parent.Items, oldName) 347 f.saveFolders() 348 return nil 349 } 350 351 func (f *folders) folderDelete(cd ConnData, data json.RawMessage) error { 352 var folder string 353 if err := json.Unmarshal(data, &folder); err != nil { 354 return err 355 } 356 f.mu.Lock() 357 defer f.mu.Unlock() 358 parent, oldName, fd := f.getParentFolder(folder) 359 if parent == nil || fd == nil { 360 return ErrFolderNotFound 361 } 362 delete(parent.Folders, oldName) 363 f.saveFolders() 364 f.socket.broadcastAdminChange(f.getBroadcastID(broadcastImageFolderRemove), data, cd.ID) 365 return nil 366 } 367 368 func (f *folders) copyItem(cd ConnData, data json.RawMessage) (interface{}, error) { 369 var ip struct { 370 ID uint64 `json:"id"` 371 Path string `json:"path"` 372 } 373 if err := json.Unmarshal(data, &ip); err != nil { 374 return "", err 375 } 376 f.mu.Lock() 377 parent, name, _ := f.getFolderItem(ip.Path) 378 if parent == nil { 379 f.mu.Unlock() 380 return "", ErrFolderNotFound 381 } 382 if name == "" { 383 name = strconv.FormatUint(ip.ID, 10) 384 } 385 newName := addItemTo(parent.Items, name, ip.ID) 386 f.saveFolders() 387 f.mu.Unlock() 388 ip.Path = ip.Path[:len(ip.Path)-len(name)] + newName 389 data = append(appendString(append(strconv.AppendUint(append(strconv.AppendUint(append(data[:0], "{\"oldID\":"...), ip.ID, 10), ",\"newID\":"...), ip.ID, 10), ",\"path\":"...), ip.Path), '}') 390 f.socket.broadcastAdminChange(f.getBroadcastID(broadcastImageItemCopy), data, cd.ID) 391 data = append(appendString(append(strconv.AppendUint(append(data[:0], "{\"id\":"...), ip.ID, 10), ",\"path\":"...), ip.Path), '}') 392 return data, nil 393 } 394 395 func (f *folders) getBroadcastID(base int) int { 396 switch f.fileType { 397 case fileTypeAudio: 398 return base - 1 399 case fileTypeCharacter: 400 return base - 2 401 case fileTypeMap: 402 return base - 3 403 } 404 return base 405 } 406 407 const folderMetadata = "folders" 408