Compare commits
3 Commits
cbf7218453
...
3ea2227a9f
Author | SHA1 | Date |
---|---|---|
Seednode | 3ea2227a9f | |
Seednode | b5d08b5b6d | |
Seednode | d7bc6e2451 |
18
README.md
|
@ -8,8 +8,6 @@ A new file will be selected if you open `/` directly, or if you click on any dis
|
|||
|
||||
Browser history is preserved, so you can always go back to any previously displayed media.
|
||||
|
||||
Supported file types and extensions are listed in the corresponding source files in `formats/`.
|
||||
|
||||
Feature requests, code criticism, bug reports, general chit-chat, and unrelated angst accepted at `roulette@seedno.de`.
|
||||
|
||||
Static binary builds available [here](https://cdn.seedno.de/builds/roulette).
|
||||
|
@ -78,13 +76,17 @@ The cache can be regenerated at any time by accessing the `/clear_cache` endpoin
|
|||
|
||||
If `--cache-file` is set, the cache will be loaded from the specified file on start, and written to the file whenever it is re-generated.
|
||||
|
||||
If the `-i|--index` flag is passed, four additional endpoints are registered.
|
||||
## Info
|
||||
|
||||
If the `-i|--info` flag is passed, six additional endpoints are registered.
|
||||
|
||||
The first of these—`/html` and `/json`—return the contents of the index, in HTML and JSON formats respectively.
|
||||
|
||||
If `--page-length` is also set, these can be viewed in paginated form by appending `/n`, e.g. `/html/5` for the fifth page.
|
||||
|
||||
This can prove useful when confirming whether the index is generated successfully, or whether a given file is in the index.
|
||||
|
||||
The other two endpoints—`/extensions` and `/mime_types`—return the registered file types.
|
||||
The remaining four endpoints—`/available_extensions`, `/enabled_extensions`, `/available_mime_types` and `/enabled_mime_types`—return information about the registered file types.
|
||||
|
||||
## Statistics
|
||||
|
||||
|
@ -92,6 +94,8 @@ If the `--stats` flag is passed, an additional endpoint, `/stats`, is registered
|
|||
|
||||
When accessed, this endpoint returns a JSON document listing every file served, along with the number of times it has been served, its filesize, and timestamps of when it was served.
|
||||
|
||||
If `--page-length` is also set, this can be viewed in paginated form by appending `/n`, e.g. `/stats/5` for the fifth page.
|
||||
|
||||
## Russian
|
||||
If the `--russian` flag is passed, everything functions exactly as you would expect.
|
||||
|
||||
|
@ -111,7 +115,7 @@ Usage:
|
|||
roulette <path> [path]... [flags]
|
||||
|
||||
Flags:
|
||||
--all enable all supported file types
|
||||
-a, --all enable all supported file types
|
||||
--audio enable support for audio files
|
||||
-b, --bind string address to bind to (default "0.0.0.0")
|
||||
-c, --cache generate directory cache at startup
|
||||
|
@ -120,7 +124,7 @@ Flags:
|
|||
--flash enable support for shockwave flash files (via ruffle.rs)
|
||||
-h, --help help for roulette
|
||||
--images enable support for image files
|
||||
-i, --index expose index endpoints
|
||||
-i, --info expose informational endpoints
|
||||
--maximum-files uint32 skip directories with file counts above this value (default 4294967295)
|
||||
--minimum-files uint32 skip directories with file counts below this value (default 1)
|
||||
--page-length uint32 pagination length for statistics and debug pages
|
||||
|
@ -133,7 +137,7 @@ Flags:
|
|||
--stats expose stats endpoint
|
||||
--stats-file string path to optional persistent stats file
|
||||
--text enable support for text files
|
||||
-v, --verbose log accessed files to stdout
|
||||
-v, --verbose log accessed files and other information to stdout
|
||||
-V, --version display version and exit
|
||||
--video enable support for video files
|
||||
```
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
Copyright © 2023 Seednode <seednode@seedno.de>
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"seedno.de/seednode/roulette/types"
|
||||
)
|
||||
|
||||
type FileIndex struct {
|
||||
mutex sync.RWMutex
|
||||
list []string
|
||||
}
|
||||
|
||||
func (i *FileIndex) Index() []string {
|
||||
i.mutex.RLock()
|
||||
val := i.list
|
||||
i.mutex.RUnlock()
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (i *FileIndex) Remove(path string) {
|
||||
i.mutex.RLock()
|
||||
tempIndex := make([]string, len(i.list))
|
||||
copy(tempIndex, i.list)
|
||||
i.mutex.RUnlock()
|
||||
|
||||
var position int
|
||||
|
||||
for k, v := range tempIndex {
|
||||
if path == v {
|
||||
position = k
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tempIndex[position] = tempIndex[len(tempIndex)-1]
|
||||
|
||||
i.mutex.Lock()
|
||||
i.list = make([]string, len(tempIndex)-1)
|
||||
copy(i.list, tempIndex[:len(tempIndex)-1])
|
||||
i.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (i *FileIndex) setIndex(val []string) {
|
||||
i.mutex.Lock()
|
||||
i.list = val
|
||||
i.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (i *FileIndex) generateCache(args []string, formats *types.Types) {
|
||||
i.mutex.Lock()
|
||||
i.list = []string{}
|
||||
i.mutex.Unlock()
|
||||
|
||||
fileList(args, &Filters{}, "", i, formats)
|
||||
|
||||
if Cache && CacheFile != "" {
|
||||
i.Export(CacheFile)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *FileIndex) IsEmpty() bool {
|
||||
i.mutex.RLock()
|
||||
length := len(i.list)
|
||||
i.mutex.RUnlock()
|
||||
|
||||
return length == 0
|
||||
}
|
||||
|
||||
func (i *FileIndex) Export(path string) error {
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
z, err := zstd.NewWriter(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer z.Close()
|
||||
|
||||
enc := gob.NewEncoder(z)
|
||||
|
||||
i.mutex.RLock()
|
||||
|
||||
enc.Encode(&i.list)
|
||||
|
||||
i.mutex.RUnlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *FileIndex) Import(path string) error {
|
||||
file, err := os.OpenFile(path, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
z, err := zstd.NewReader(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer z.Close()
|
||||
|
||||
dec := gob.NewDecoder(z)
|
||||
|
||||
i.mutex.Lock()
|
||||
|
||||
err = dec.Decode(&i.list)
|
||||
|
||||
i.mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func serveCacheClear(args []string, index *FileIndex, formats *types.Types) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
index.generateCache(args, formats)
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
w.Write([]byte("Ok"))
|
||||
}
|
||||
}
|
|
@ -5,150 +5,20 @@ Copyright © 2023 Seednode <seednode@seedno.de>
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/yosssi/gohtml"
|
||||
"seedno.de/seednode/roulette/types"
|
||||
)
|
||||
|
||||
type FileIndex struct {
|
||||
mutex sync.RWMutex
|
||||
list []string
|
||||
}
|
||||
|
||||
func (i *FileIndex) Index() []string {
|
||||
i.mutex.RLock()
|
||||
val := i.list
|
||||
i.mutex.RUnlock()
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (i *FileIndex) Remove(path string) {
|
||||
i.mutex.RLock()
|
||||
tempIndex := make([]string, len(i.list))
|
||||
copy(tempIndex, i.list)
|
||||
i.mutex.RUnlock()
|
||||
|
||||
var position int
|
||||
|
||||
for k, v := range tempIndex {
|
||||
if path == v {
|
||||
position = k
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tempIndex[position] = tempIndex[len(tempIndex)-1]
|
||||
|
||||
i.mutex.Lock()
|
||||
i.list = make([]string, len(tempIndex)-1)
|
||||
copy(i.list, tempIndex[:len(tempIndex)-1])
|
||||
i.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (i *FileIndex) setIndex(val []string) {
|
||||
i.mutex.Lock()
|
||||
i.list = val
|
||||
i.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (i *FileIndex) generateCache(args []string, formats *types.Types) {
|
||||
i.mutex.Lock()
|
||||
i.list = []string{}
|
||||
i.mutex.Unlock()
|
||||
|
||||
fileList(args, &Filters{}, "", i, formats)
|
||||
|
||||
if Cache && CacheFile != "" {
|
||||
i.Export(CacheFile)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *FileIndex) IsEmpty() bool {
|
||||
i.mutex.RLock()
|
||||
length := len(i.list)
|
||||
i.mutex.RUnlock()
|
||||
|
||||
return length == 0
|
||||
}
|
||||
|
||||
func (i *FileIndex) Export(path string) error {
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
z, err := zstd.NewWriter(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer z.Close()
|
||||
|
||||
enc := gob.NewEncoder(z)
|
||||
|
||||
i.mutex.RLock()
|
||||
|
||||
enc.Encode(&i.list)
|
||||
|
||||
i.mutex.RUnlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *FileIndex) Import(path string) error {
|
||||
file, err := os.OpenFile(path, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
z, err := zstd.NewReader(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer z.Close()
|
||||
|
||||
dec := gob.NewDecoder(z)
|
||||
|
||||
i.mutex.Lock()
|
||||
|
||||
err = dec.Decode(&i.list)
|
||||
|
||||
i.mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func serveCacheClear(args []string, index *FileIndex, formats *types.Types) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
index.generateCache(args, formats)
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
w.Write([]byte("Ok"))
|
||||
}
|
||||
}
|
||||
|
||||
func serveIndexHtml(args []string, index *FileIndex, paginate bool) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
@ -318,30 +188,34 @@ func serveIndexJson(args []string, index *FileIndex) httprouter.Handle {
|
|||
}
|
||||
}
|
||||
|
||||
func serveExtensions(formats *types.Types) httprouter.Handle {
|
||||
func serveAvailableExtensions() httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
var output strings.Builder
|
||||
response := []byte(types.SupportedFormats.GetExtensions())
|
||||
|
||||
extensions := make([]string, len(formats.Extensions))
|
||||
w.Write(response)
|
||||
|
||||
i := 0
|
||||
|
||||
for k := range formats.Extensions {
|
||||
extensions[i] = k
|
||||
i++
|
||||
if Verbose {
|
||||
fmt.Printf("%s | Served available extensions list (%s) to %s in %s\n",
|
||||
startTime.Format(LogDate),
|
||||
humanReadableSize(len(response)),
|
||||
realIP(r),
|
||||
time.Since(startTime).Round(time.Microsecond),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(extensions)
|
||||
func serveEnabledExtensions(formats *types.Types) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
for _, v := range extensions {
|
||||
output.WriteString(v + "\n")
|
||||
}
|
||||
startTime := time.Now()
|
||||
|
||||
response := []byte(output.String())
|
||||
response := []byte(formats.GetExtensions())
|
||||
|
||||
w.Write(response)
|
||||
|
||||
|
@ -356,30 +230,34 @@ func serveExtensions(formats *types.Types) httprouter.Handle {
|
|||
}
|
||||
}
|
||||
|
||||
func serveMimeTypes(formats *types.Types) httprouter.Handle {
|
||||
func serveAvailableMimeTypes() httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
var output strings.Builder
|
||||
response := []byte(types.SupportedFormats.GetMimeTypes())
|
||||
|
||||
mimeTypes := make([]string, len(formats.MimeTypes))
|
||||
w.Write(response)
|
||||
|
||||
i := 0
|
||||
|
||||
for k := range formats.MimeTypes {
|
||||
mimeTypes[i] = k
|
||||
i++
|
||||
if Verbose {
|
||||
fmt.Printf("%s | Served available MIME types list (%s) to %s in %s\n",
|
||||
startTime.Format(LogDate),
|
||||
humanReadableSize(len(response)),
|
||||
realIP(r),
|
||||
time.Since(startTime).Round(time.Microsecond),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(mimeTypes)
|
||||
func serveEnabledMimeTypes(formats *types.Types) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
for _, v := range mimeTypes {
|
||||
output.WriteString(v + "\n")
|
||||
}
|
||||
startTime := time.Now()
|
||||
|
||||
response := []byte(output.String())
|
||||
response := []byte(formats.GetMimeTypes())
|
||||
|
||||
w.Write(response)
|
||||
|
14
cmd/root.go
|
@ -12,7 +12,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
ReleaseVersion string = "0.75.0"
|
||||
ReleaseVersion string = "0.76.0"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -24,7 +24,7 @@ var (
|
|||
Filtering bool
|
||||
Flash bool
|
||||
Images bool
|
||||
Index bool
|
||||
Info bool
|
||||
MaximumFileCount uint32
|
||||
MinimumFileCount uint32
|
||||
PageLength uint32
|
||||
|
@ -46,10 +46,6 @@ var (
|
|||
Short: "Serves random media from the specified directories.",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if Index {
|
||||
cmd.MarkFlagRequired("cache")
|
||||
}
|
||||
|
||||
if RefreshInterval != "" {
|
||||
interval, err := time.ParseDuration(RefreshInterval)
|
||||
if err != nil || interval < 500*time.Millisecond {
|
||||
|
@ -78,7 +74,7 @@ func Execute() {
|
|||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Flags().BoolVar(&All, "all", false, "enable all supported file types")
|
||||
rootCmd.Flags().BoolVarP(&All, "all", "a", false, "enable all supported file types")
|
||||
rootCmd.Flags().BoolVar(&Audio, "audio", false, "enable support for audio files")
|
||||
rootCmd.Flags().StringVarP(&Bind, "bind", "b", "0.0.0.0", "address to bind to")
|
||||
rootCmd.Flags().BoolVarP(&Cache, "cache", "c", false, "generate directory cache at startup")
|
||||
|
@ -86,7 +82,7 @@ func init() {
|
|||
rootCmd.Flags().BoolVarP(&Filtering, "filter", "f", false, "enable filtering")
|
||||
rootCmd.Flags().BoolVar(&Flash, "flash", false, "enable support for shockwave flash files (via ruffle.rs)")
|
||||
rootCmd.Flags().BoolVar(&Images, "images", false, "enable support for image files")
|
||||
rootCmd.Flags().BoolVarP(&Index, "index", "i", false, "expose index endpoints")
|
||||
rootCmd.Flags().BoolVarP(&Info, "info", "i", false, "expose informational endpoints")
|
||||
rootCmd.Flags().Uint32Var(&MaximumFileCount, "maximum-files", 1<<32-1, "skip directories with file counts above this value")
|
||||
rootCmd.Flags().Uint32Var(&MinimumFileCount, "minimum-files", 1, "skip directories with file counts below this value")
|
||||
rootCmd.Flags().Uint32Var(&PageLength, "page-length", 0, "pagination length for statistics and debug pages")
|
||||
|
@ -99,7 +95,7 @@ func init() {
|
|||
rootCmd.Flags().BoolVar(&Statistics, "stats", false, "expose stats endpoint")
|
||||
rootCmd.Flags().StringVar(&StatisticsFile, "stats-file", "", "path to optional persistent stats file")
|
||||
rootCmd.Flags().BoolVar(&Text, "text", false, "enable support for text files")
|
||||
rootCmd.Flags().BoolVarP(&Verbose, "verbose", "v", false, "log accessed files to stdout")
|
||||
rootCmd.Flags().BoolVarP(&Verbose, "verbose", "v", false, "log accessed files and other information to stdout")
|
||||
rootCmd.Flags().BoolVarP(&Version, "version", "V", false, "display version and exit")
|
||||
rootCmd.Flags().BoolVar(&Videos, "video", false, "enable support for video files")
|
||||
|
||||
|
|
42
cmd/web.go
|
@ -27,6 +27,11 @@ import (
|
|||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/yosssi/gohtml"
|
||||
"seedno.de/seednode/roulette/types"
|
||||
"seedno.de/seednode/roulette/types/audio"
|
||||
"seedno.de/seednode/roulette/types/flash"
|
||||
"seedno.de/seednode/roulette/types/images"
|
||||
"seedno.de/seednode/roulette/types/text"
|
||||
"seedno.de/seednode/roulette/types/video"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -317,25 +322,25 @@ func ServePage(args []string) error {
|
|||
}
|
||||
|
||||
if Audio || All {
|
||||
formats.Add(types.Audio{})
|
||||
formats.Add(audio.Format{})
|
||||
}
|
||||
|
||||
if Flash || All {
|
||||
formats.Add(types.Flash{})
|
||||
formats.Add(flash.Format{})
|
||||
}
|
||||
|
||||
if Text || All {
|
||||
formats.Add(types.Text{})
|
||||
formats.Add(text.Format{})
|
||||
}
|
||||
|
||||
if Videos || All {
|
||||
formats.Add(types.Video{})
|
||||
formats.Add(video.Format{})
|
||||
}
|
||||
|
||||
// enable image support if no other flags are passed, to retain backwards compatibility
|
||||
// to be replaced with rootCmd.MarkFlagsOneRequired on next spf13/cobra update
|
||||
if Images || All || len(formats.Extensions) == 0 {
|
||||
formats.Add(types.Images{})
|
||||
formats.Add(images.Format{})
|
||||
}
|
||||
|
||||
paths, err := normalizePaths(args, formats)
|
||||
|
@ -408,20 +413,23 @@ func ServePage(args []string) error {
|
|||
mux.GET("/clear_cache", serveCacheClear(args, index, formats))
|
||||
}
|
||||
|
||||
if Index {
|
||||
mux.GET("/html/", serveIndexHtml(args, index, false))
|
||||
if PageLength != 0 {
|
||||
mux.GET("/html/:page", serveIndexHtml(args, index, true))
|
||||
if Info {
|
||||
if Cache {
|
||||
mux.GET("/html/", serveIndexHtml(args, index, false))
|
||||
if PageLength != 0 {
|
||||
mux.GET("/html/:page", serveIndexHtml(args, index, true))
|
||||
}
|
||||
|
||||
mux.GET("/json", serveIndexJson(args, index))
|
||||
if PageLength != 0 {
|
||||
mux.GET("/json/:page", serveIndexJson(args, index))
|
||||
}
|
||||
}
|
||||
|
||||
mux.GET("/json", serveIndexJson(args, index))
|
||||
if PageLength != 0 {
|
||||
mux.GET("/json/:page", serveIndexJson(args, index))
|
||||
}
|
||||
|
||||
mux.GET("/extensions", serveExtensions(formats))
|
||||
|
||||
mux.GET("/mime_types", serveMimeTypes(formats))
|
||||
mux.GET("/available_extensions", serveAvailableExtensions())
|
||||
mux.GET("/enabled_extensions", serveEnabledExtensions(formats))
|
||||
mux.GET("/available_mime_types", serveAvailableMimeTypes())
|
||||
mux.GET("/enabled_mime_types", serveEnabledMimeTypes(formats))
|
||||
}
|
||||
|
||||
if Profile {
|
||||
|
|
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 96 KiB |