summaryrefslogtreecommitdiff
path: root/store/image.go
blob: 8daae1c5069ad007e68914b6733006937a9d68de (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
package store

import (
	"bytes"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"github.com/nfnt/resize"
	log "github.com/sirupsen/logrus"
	"image"
	_ "image/gif"
	"image/jpeg"
	_ "image/png"
	"os"
)

const ImageRootPageVariant = "root"

// Image represents metadata of an image.
type Image struct {
	Snowflake             string `json:"snowflake"`
	Hash                  string `json:"hash"`
	Type                  string `json:"type"`
	User                  string `json:"user"`
	Source                string `json:"source"`
	Parent                string `json:"parent"`
	Child                 string `json:"child"`
	Commentary            string `json:"commentary"`
	CommentaryTranslation string `json:"commentary_translation"`
}

// MakePreview compresses an image.Image to preview-size.
func MakePreview(img image.Image) image.Image {
	return resize.Thumbnail(256, 256, img, resize.Bilinear)
}

// Images returns a slice of image hashes.
func (s *Store) Images() []string {
	var images []string
	if entries, err := os.ReadDir(s.ImagesDir()); err != nil {
		s.fatalClose(fmt.Sprintf("Error reading first level image directory, %s", err))
	} else {
		for _, entry := range entries {
			if entry.IsDir() {
				var subEntries []os.DirEntry
				if subEntries, err = os.ReadDir(s.ImagesDir() + "/" + entry.Name()); err != nil {
					s.fatalClose(fmt.Sprintf("Error reading second level image directory %s, %s", entry.Name(), err))
				} else {
					for _, subEntry := range subEntries {
						images = append(images, entry.Name()+subEntry.Name())
					}
				}
			}
		}
	}
	return images
}

// Image returns an image with specific hash.
func (s *Store) Image(hash string) Image {
	if !sha256Regex.MatchString(hash) || !s.file(s.ImageMetadataPath(hash)) {
		return Image{}
	}

	s.getLock(hash).RLock()
	defer s.getLock(hash).RUnlock()
	return s.ImageMetadataRead(s.ImageMetadataPath(hash))
}

// ImageMetadataRead reads an image metadata file.
func (s *Store) ImageMetadataRead(path string) Image {
	var metadata Image
	if payload, err := os.ReadFile(path); err != nil {
		if os.IsNotExist(err) {
			return Image{}
		}
		s.fatalClose(fmt.Sprintf("Error reading image metadata %s, %s", path, err))
	} else {
		if err = json.Unmarshal(payload, &metadata); err != nil {
			s.fatalClose(fmt.Sprintf("Error parsing image metadata %s, %s", path, err))
		}
	}
	return metadata
}

// ImageData returns an image and its data with a specific hash.
func (s *Store) ImageData(hash string, preview bool) (Image, []byte) {
	if !sha256Regex.MatchString(hash) || !s.file(s.ImageMetadataPath(hash)) {
		return Image{}, nil
	}

	s.getLock(hash).RLock()
	defer s.getLock(hash).RUnlock()
	var metadata Image
	if payload, err := os.ReadFile(s.ImageMetadataPath(hash)); err != nil {
		s.fatalClose(fmt.Sprintf("Error reading image %s metadata, %s", hash, err))
	} else {
		if err = json.Unmarshal(payload, &metadata); err != nil {
			s.fatalClose(fmt.Sprintf("Error parsing image %s metadata, %s", hash, err))
		}
	}
	var path string
	if !preview {
		path = s.ImageFilePath(hash)
	} else {
		path = s.ImagePreviewFilePath(hash)
	}
	if data, err := os.ReadFile(path); err != nil {
		s.fatalClose(fmt.Sprintf("Error reading image %s file, %s", hash, err))
	} else {
		return metadata, data
	}
	return Image{}, nil
}

// ImageTags returns tags of an image with specific hash.
func (s *Store) ImageTags(flake string) []string {
	if !numerical(flake) || !s.dir(s.ImageTagsPath(flake)) {
		return nil
	}

	s.getLock(flake).RLock()
	defer s.getLock(flake).RUnlock()
	return s.imageTags(flake)
}

func (s *Store) imageTags(flake string) []string {
	var tags []string
	if entries, err := os.ReadDir(s.ImageTagsPath(flake)); err != nil {
		s.fatalClose(fmt.Sprintf("Error reading tags of image %s, %s", flake, err))
	} else {
		for _, entry := range entries {
			tags = append(tags, entry.Name())
		}
	}
	return tags
}

// ImageHasTag figures out if an image has a tag.
func (s *Store) ImageHasTag(flake, tag string) bool {
	if !numerical(flake) || !nameRegex.MatchString(tag) {
		return false
	}
	return s.file(s.ImageTagsPath(flake) + "/" + tag)
}

//ImageSearch searches for images with specific tags.
func (s *Store) ImageSearch(tags []string) []string {
	if len(tags) < 1 || tags == nil {
		return nil
	}

	// Check if every tag matches name regex and exists
	for _, tag := range tags {
		if !nameRegex.MatchString(tag) || !s.file(s.TagPath(tag)) {
			return nil
		}
	}

	// Return if there's only one tag to search for
	if len(tags) == 1 {
		return s.Tag(tags[0])
	}

	// Find entry with the least pages
	entry := struct {
		min   int
		index int
	}{
		min:   s.PageTotal("tag_" + tags[0]),
		index: 0,
	}
	for i := 1; i < len(tags); i++ {
		if entry.min <= 1 {
			break
		}

		pages := s.PageTotal("tag_" + tags[i])
		if pages < entry.min {
			entry.min = pages
			entry.index = i
		}
	}

	// Get initial tag
	initial := s.Tag(tags[entry.index])

	// Result slice
	var result []string

	// Walk flakes from initial tag
	for _, flake := range initial {
		match := true
		// Walk all remaining tags
		for i, tag := range tags {
			// Skip the entrypoint entry
			if i == entry.index {
				continue
			}

			// Check if match
			if !s.ImageHasTag(flake, tag) {
				match = false
				break
			}
		}

		// Append flake if all tags matched
		if match {
			result = append(result, flake)
		}
	}

	return result
}

// ImageAdd adds an image to the store.
func (s *Store) ImageAdd(data []byte, flake string) Image {
	if !numerical(flake) || !s.dir(s.UserPath(flake)) {
		return Image{}
	}

	// Create image info and set time
	info := Image{Snowflake: imageNode.Generate().String(), User: flake}

	// Calculate sha256 and convert to string
	info.Hash = fmt.Sprintf("%x", sha256.Sum256(data))

	// Return existing image if already exists.
	e := s.Image(info.Hash)
	if e.Hash == info.Hash {
		return e
	}

	s.getLock(info.Hash).Lock()
	defer s.getLock(info.Hash).Unlock()

	// Decode image and set format
	var img image.Image
	if i, format, err := image.Decode(bytes.NewReader(data)); err != nil {
		log.Warnf("Error decoding upload %s, %s", info.Hash, err)
		return Image{}
	} else {
		img = MakePreview(i)
		info.Type = format
	}

	// Create image directory and tags
	if err := os.MkdirAll(s.ImageHashTagsPath(info.Hash), s.PermissionDir); err != nil {
		s.fatalClose(fmt.Sprintf("Error creating image %s directory, %s", info.Hash, err))
	}

	// Generate and save image metadata
	if payload, err := json.Marshal(info); err != nil {
		s.fatalClose(fmt.Sprintf("Error encoding metadata of image %s, %s", info.Hash, err))
	} else {
		if err = os.WriteFile(s.ImageMetadataPath(info.Hash), payload, s.PermissionFile); err != nil {
			s.fatalClose(fmt.Sprintf("Error saving metadata of image %s, %s", info.Hash, err))
		}
	}

	// Save image
	if err := os.WriteFile(s.ImageFilePath(info.Hash), data, s.PermissionFile); err != nil {
		s.fatalClose(fmt.Sprintf("Error saving image %s, %s", info.Hash, err))
	}

	// Save preview image
	if preview, err := os.Create(s.ImagePreviewFilePath(info.Hash)); err != nil {
		s.fatalClose(fmt.Sprintf("Error creating image %s preview, %s", info.Hash, err))
	} else {
		if err = jpeg.Encode(preview, img, &jpeg.Options{Quality: 100}); err != nil {
			s.fatalClose(fmt.Sprintf("Error saving image %s preview, %s", info.Hash, err))
		}
		if err = preview.Close(); err != nil {
			s.fatalClose(fmt.Sprintf("Error closing image %s preview, %s", info.Hash, err))
		}
	}

	// Symbolically link image directory to snowflake directory
	s.link("../images/"+s.ImageHashSplit(info.Hash), s.ImageSnowflakePath(info.Snowflake))
	s.link("../../../images/"+s.ImageHashSplit(info.Hash), s.UserImagesPath(flake)+"/"+info.Snowflake)

	// Insert image into page index
	go s.PageInsert(ImageRootPageVariant, info.Snowflake)

	log.Infof("Image hash %s snowflake %s type %s added by user %s.", info.Hash, info.Snowflake, info.Type, info.User)
	return info
}

// ImageUpdate updates image metadata.
func (s *Store) ImageUpdate(hash, source, parent, commentary, commentaryTranslation string) {
	// Only accept URLs and below 1024 in length
	if len(source) >= 1024 {
		return
	}

	// Only accept commentary and translations below 65536 in length
	if len(commentary) >= 65536 || len(commentaryTranslation) >= 65536 {
		return
	}

	// Get info
	info := s.Image(hash)
	if info.Hash != hash {
		return
	}

	s.getLock(hash).Lock()
	defer s.getLock(hash).Unlock()

	var msg string

	// Update and save
	if source != "\000" && s.MatchURL(source) {
		info.Source = source
		msg += "source"
	}
	if parent != "\000" && parent != "" && parent != info.Snowflake {
		if p := s.ImageSnowflake(parent); p.Snowflake == parent {
			s.getLock(p.Hash).Lock()
			defer s.getLock(p.Hash).Unlock()

			info.Parent = parent

			// Update the parent to reflect the child
			p.Child = info.Snowflake
			s.imageMetadataWrite(p)

			if msg != "" {
				msg += ", "
			}
			msg += "parent " + parent
		}
	}
	if commentary != "\000" {
		info.Commentary = commentary

		if msg != "" {
			msg += ", "
		}
		msg += "commentary"
	}
	if commentaryTranslation != "\000" {
		info.CommentaryTranslation = commentaryTranslation

		if msg != "" {
			msg += ", "
		}
		msg += "commentary translation"
	}

	if msg != "" {
		s.imageMetadataWrite(info)
		log.Infof("Image %s %s updated.", info.Snowflake, msg)
	}
}

func (s *Store) imageMetadataWrite(info Image) {
	if payload, err := json.Marshal(info); err != nil {
		s.fatalClose(fmt.Sprintf("Error encoding metadata of image %s, %s", info.Hash, err))
	} else {
		if err = os.WriteFile(s.ImageMetadataPath(info.Hash), payload, s.PermissionFile); err != nil {
			s.fatalClose(fmt.Sprintf("Error saving metadata of image %s, %s", info.Hash, err))
		}
	}
}

// ImageSnowflakes returns a slice of image snowflakes.
func (s *Store) ImageSnowflakes() []string {
	var snowflakes []string
	if entries, err := os.ReadDir(s.ImagesSnowflakeDir()); err != nil {
		s.fatalClose(fmt.Sprintf("Error reading snowflakes, %s", err))
	} else {
		for _, entry := range entries {
			snowflakes = append(snowflakes, entry.Name())
		}
	}
	return snowflakes
}

// ImageSnowflakeHash returns image hash from snowflake.
func (s *Store) ImageSnowflakeHash(flake string) string {
	if !numerical(flake) {
		return ""
	}

	if !s.Compat {
		return s.ImageMetadataRead(s.ImageSnowflakePath(flake) + "/" + infoJson).Hash
	} else {
		if path, err := os.ReadFile(s.ImageSnowflakePath(flake)); err != nil {
			if os.IsNotExist(err) {
				return ""
			}
			s.fatalClose(fmt.Sprintf("Error reading snowflake %s association file, %s", flake, err))
		} else {
			return s.ImageMetadataRead(string(path) + "/" + infoJson).Hash
		}
	}
	return ""
}

// ImageSnowflake returns image that has specific snowflake.
func (s *Store) ImageSnowflake(flake string) Image {
	return s.Image(s.ImageSnowflakeHash(flake))
}

// ImageDestroy destroys an image.
func (s *Store) ImageDestroy(hash string) {
	if !sha256Regex.MatchString(hash) || !s.dir(s.ImagePath(hash)) {
		return
	}

	s.getLock(hash).Lock()
	defer s.getLock(hash).Unlock()

	info := s.ImageMetadataRead(s.ImageMetadataPath(hash))

	// Untag the image completely
	tags := s.imageTags(info.Snowflake)
	for _, tag := range tags {
		s.imageTagRemove(info.Snowflake, tag)
	}

	// Remove snowflake
	if err := os.Remove(s.ImageSnowflakePath(info.Snowflake)); err != nil {
		s.fatalClose(fmt.Sprintf("Error destroying snowflake %s of image %s, %s", info.Snowflake, hash, err))
	}

	// Disassociate user
	if err := os.Remove(s.UserImagesPath(info.User) + "/" + info.Snowflake); err != nil {
		s.fatalClose(fmt.Sprintf("Error destroying association %s with user %s, %s.", info.Snowflake, info.User, err))
	}

	// Remove data directory
	if err := os.RemoveAll(s.ImagePath(hash)); err != nil {
		s.fatalClose(fmt.Sprintf("Error destroying image %s, %s", hash, err))
	}

	// Register remove counter
	s.PageRegisterRemove(ImageRootPageVariant, info.Snowflake)

	log.Infof("Image hash %s snowflake %s destroyed.", info.Hash, info.Snowflake)
}

// ImageTagAdd adds a tag to an image with specific snowflake.
func (s *Store) ImageTagAdd(flake, tag string) {
	if !nameRegex.MatchString(tag) || !numerical(flake) || !s.dir(s.ImageTagsPath(flake)) || !s.dir(s.TagPath(tag)) ||
		s.file(s.TagPath(tag)+"/"+flake) {
		return
	}

	s.getLock(flake).Lock()
	defer s.getLock(flake).Unlock()

	s.link("../../snowflakes/"+flake, s.TagPath(tag)+"/"+flake)
	s.link("../../../../tags/"+tag, s.ImageSnowflakePath(flake)+"/tags/"+tag)
	s.PageInsert("tag_"+tag, flake)
	log.Infof("Image snowflake %s tagged with %s.", flake, tag)
}

// ImageTagRemove removes a tag from an image with specific snowflake.
func (s *Store) ImageTagRemove(flake, tag string) {
	if !nameRegex.MatchString(tag) || !numerical(flake) || !s.dir(s.ImageTagsPath(flake)) || !s.dir(s.TagPath(tag)) {
		return
	}

	s.getLock(flake).Lock()
	defer s.getLock(flake).Unlock()

	s.imageTagRemove(flake, tag)
}

func (s *Store) imageTagRemove(flake, tag string) {
	if s.file(s.ImageTagsPath(flake) + "/" + tag) {
		if err := os.Remove(s.ImageTagsPath(flake) + "/" + tag); err != nil {
			s.fatalClose(fmt.Sprintf("Error unreferencing image %s from tag %s, %s", flake, tag, err))
		}
	}
	if s.file(s.TagPath(tag) + "/" + flake) {
		if err := os.Remove(s.TagPath(tag) + "/" + flake); err != nil {
			s.fatalClose(fmt.Sprintf("Error unreferencing tag %s from image %s, %s", tag, flake, err))
		}
	}
	s.PageRegisterRemove("tag_"+tag, flake)
	log.Infof("Image snowflake %s untagged %s.", flake, tag)
}