mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -05:00
249 lines
4.9 KiB
Go
249 lines
4.9 KiB
Go
package clipboard
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"image"
|
|
_ "image/gif"
|
|
_ "image/jpeg"
|
|
_ "image/png"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "golang.org/x/image/bmp"
|
|
_ "golang.org/x/image/tiff"
|
|
"hash/fnv"
|
|
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
type StoreConfig struct {
|
|
MaxHistory int
|
|
MaxEntrySize int64
|
|
}
|
|
|
|
func DefaultStoreConfig() StoreConfig {
|
|
return StoreConfig{
|
|
MaxHistory: 100,
|
|
MaxEntrySize: 5 * 1024 * 1024,
|
|
}
|
|
}
|
|
|
|
type Entry struct {
|
|
ID uint64
|
|
Data []byte
|
|
MimeType string
|
|
Preview string
|
|
Size int
|
|
Timestamp time.Time
|
|
IsImage bool
|
|
Hash uint64
|
|
}
|
|
|
|
func Store(data []byte, mimeType string) error {
|
|
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
|
|
}
|
|
|
|
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
if int64(len(data)) > cfg.MaxEntrySize {
|
|
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
|
|
}
|
|
|
|
dbPath, err := GetDBPath()
|
|
if err != nil {
|
|
return fmt.Errorf("get db path: %w", err)
|
|
}
|
|
|
|
db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: 1 * time.Second})
|
|
if err != nil {
|
|
return fmt.Errorf("open db: %w", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
entry := Entry{
|
|
Data: data,
|
|
MimeType: mimeType,
|
|
Size: len(data),
|
|
Timestamp: time.Now(),
|
|
IsImage: IsImageMimeType(mimeType),
|
|
Hash: computeHash(data),
|
|
}
|
|
|
|
switch {
|
|
case entry.IsImage:
|
|
entry.Preview = imagePreview(data, mimeType)
|
|
default:
|
|
entry.Preview = textPreview(data)
|
|
}
|
|
|
|
return db.Update(func(tx *bolt.Tx) error {
|
|
b, err := tx.CreateBucketIfNotExists([]byte("clipboard"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := deduplicateInTx(b, entry.Hash); err != nil {
|
|
return err
|
|
}
|
|
|
|
id, err := b.NextSequence()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entry.ID = id
|
|
|
|
encoded, err := encodeEntry(entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := b.Put(itob(id), encoded); err != nil {
|
|
return err
|
|
}
|
|
|
|
return trimLengthInTx(b, cfg.MaxHistory)
|
|
})
|
|
}
|
|
|
|
func GetDBPath() (string, error) {
|
|
cacheDir, err := os.UserCacheDir()
|
|
if err != nil {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cacheDir = filepath.Join(homeDir, ".cache")
|
|
}
|
|
|
|
newDir := filepath.Join(cacheDir, "DankMaterialShell", "clipboard")
|
|
newPath := filepath.Join(newDir, "db")
|
|
|
|
if _, err := os.Stat(newPath); err == nil {
|
|
return newPath, nil
|
|
}
|
|
|
|
oldDir := filepath.Join(cacheDir, "dms-clipboard")
|
|
oldPath := filepath.Join(oldDir, "db")
|
|
|
|
if _, err := os.Stat(oldPath); err == nil {
|
|
if err := os.MkdirAll(newDir, 0700); err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.Rename(oldPath, newPath); err != nil {
|
|
return "", err
|
|
}
|
|
os.Remove(oldDir)
|
|
return newPath, nil
|
|
}
|
|
|
|
if err := os.MkdirAll(newDir, 0700); err != nil {
|
|
return "", err
|
|
}
|
|
return newPath, nil
|
|
}
|
|
|
|
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
|
c := b.Cursor()
|
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
|
if extractHash(v) != hash {
|
|
continue
|
|
}
|
|
if err := b.Delete(k); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func trimLengthInTx(b *bolt.Bucket, maxHistory int) error {
|
|
c := b.Cursor()
|
|
var count int
|
|
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
|
if count < maxHistory {
|
|
count++
|
|
continue
|
|
}
|
|
if err := b.Delete(k); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func encodeEntry(e Entry) ([]byte, error) {
|
|
buf := new(bytes.Buffer)
|
|
|
|
binary.Write(buf, binary.BigEndian, e.ID)
|
|
binary.Write(buf, binary.BigEndian, uint32(len(e.Data)))
|
|
buf.Write(e.Data)
|
|
binary.Write(buf, binary.BigEndian, uint32(len(e.MimeType)))
|
|
buf.WriteString(e.MimeType)
|
|
binary.Write(buf, binary.BigEndian, uint32(len(e.Preview)))
|
|
buf.WriteString(e.Preview)
|
|
binary.Write(buf, binary.BigEndian, int32(e.Size))
|
|
binary.Write(buf, binary.BigEndian, e.Timestamp.Unix())
|
|
if e.IsImage {
|
|
buf.WriteByte(1)
|
|
} else {
|
|
buf.WriteByte(0)
|
|
}
|
|
binary.Write(buf, binary.BigEndian, e.Hash)
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func itob(v uint64) []byte {
|
|
b := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(b, v)
|
|
return b
|
|
}
|
|
|
|
func computeHash(data []byte) uint64 {
|
|
h := fnv.New64a()
|
|
h.Write(data)
|
|
return h.Sum64()
|
|
}
|
|
|
|
func extractHash(data []byte) uint64 {
|
|
if len(data) < 8 {
|
|
return 0
|
|
}
|
|
return binary.BigEndian.Uint64(data[len(data)-8:])
|
|
}
|
|
|
|
func textPreview(data []byte) string {
|
|
text := string(data)
|
|
text = strings.TrimSpace(text)
|
|
text = strings.Join(strings.Fields(text), " ")
|
|
|
|
if len(text) > 100 {
|
|
return text[:100] + "…"
|
|
}
|
|
return text
|
|
}
|
|
|
|
func imagePreview(data []byte, format string) string {
|
|
config, imgFmt, err := image.DecodeConfig(bytes.NewReader(data))
|
|
if err != nil {
|
|
return fmt.Sprintf("[[ image %s %s ]]", sizeStr(len(data)), format)
|
|
}
|
|
return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height)
|
|
}
|
|
|
|
func sizeStr(size int) string {
|
|
units := []string{"B", "KiB", "MiB"}
|
|
var i int
|
|
fsize := float64(size)
|
|
for fsize >= 1024 && i < len(units)-1 {
|
|
fsize /= 1024
|
|
i++
|
|
}
|
|
return fmt.Sprintf("%.0f %s", fsize, units[i])
|
|
}
|