mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 23:42:51 -05:00
clipboard: scrap persist, optimize mime-type handling
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
_ "golang.org/x/image/bmp"
|
_ "golang.org/x/image/bmp"
|
||||||
_ "golang.org/x/image/tiff"
|
_ "golang.org/x/image/tiff"
|
||||||
|
"hash/fnv"
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
@@ -39,6 +40,7 @@ type Entry struct {
|
|||||||
Size int
|
Size int
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
IsImage bool
|
IsImage bool
|
||||||
|
Hash uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func Store(data []byte, mimeType string) error {
|
func Store(data []byte, mimeType string) error {
|
||||||
@@ -70,6 +72,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
|||||||
Size: len(data),
|
Size: len(data),
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
IsImage: IsImageMimeType(mimeType),
|
IsImage: IsImageMimeType(mimeType),
|
||||||
|
Hash: computeHash(data),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -85,7 +88,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := deduplicateInTx(b, data); err != nil {
|
if err := deduplicateInTx(b, entry.Hash); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,17 +129,14 @@ func getDBPath() (string, error) {
|
|||||||
return filepath.Join(dbDir, "db"), nil
|
return filepath.Join(dbDir, "db"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deduplicateInTx(b *bolt.Bucket, data []byte) error {
|
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntry(v)
|
if extractHash(v) != hash {
|
||||||
if err != nil {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if bytes.Equal(entry.Data, data) {
|
if err := b.Delete(k); err != nil {
|
||||||
if err := b.Delete(k); err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -174,54 +174,30 @@ func encodeEntry(e Entry) ([]byte, error) {
|
|||||||
} else {
|
} else {
|
||||||
buf.WriteByte(0)
|
buf.WriteByte(0)
|
||||||
}
|
}
|
||||||
|
binary.Write(buf, binary.BigEndian, e.Hash)
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeEntry(data []byte) (Entry, error) {
|
|
||||||
buf := bytes.NewReader(data)
|
|
||||||
var e Entry
|
|
||||||
|
|
||||||
binary.Read(buf, binary.BigEndian, &e.ID)
|
|
||||||
|
|
||||||
var dataLen uint32
|
|
||||||
binary.Read(buf, binary.BigEndian, &dataLen)
|
|
||||||
e.Data = make([]byte, dataLen)
|
|
||||||
buf.Read(e.Data)
|
|
||||||
|
|
||||||
var mimeLen uint32
|
|
||||||
binary.Read(buf, binary.BigEndian, &mimeLen)
|
|
||||||
mimeBytes := make([]byte, mimeLen)
|
|
||||||
buf.Read(mimeBytes)
|
|
||||||
e.MimeType = string(mimeBytes)
|
|
||||||
|
|
||||||
var prevLen uint32
|
|
||||||
binary.Read(buf, binary.BigEndian, &prevLen)
|
|
||||||
prevBytes := make([]byte, prevLen)
|
|
||||||
buf.Read(prevBytes)
|
|
||||||
e.Preview = string(prevBytes)
|
|
||||||
|
|
||||||
var size int32
|
|
||||||
binary.Read(buf, binary.BigEndian, &size)
|
|
||||||
e.Size = int(size)
|
|
||||||
|
|
||||||
var timestamp int64
|
|
||||||
binary.Read(buf, binary.BigEndian, ×tamp)
|
|
||||||
e.Timestamp = time.Unix(timestamp, 0)
|
|
||||||
|
|
||||||
var isImage byte
|
|
||||||
binary.Read(buf, binary.BigEndian, &isImage)
|
|
||||||
e.IsImage = isImage == 1
|
|
||||||
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func itob(v uint64) []byte {
|
func itob(v uint64) []byte {
|
||||||
b := make([]byte, 8)
|
b := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint64(b, v)
|
binary.BigEndian.PutUint64(b, v)
|
||||||
return b
|
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 {
|
func textPreview(data []byte) string {
|
||||||
text := string(data)
|
text := string(data)
|
||||||
text = strings.TrimSpace(text)
|
text = strings.TrimSpace(text)
|
||||||
|
|||||||
@@ -208,9 +208,6 @@ func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
if v, ok := req.Params["disableHistory"].(bool); ok {
|
if v, ok := req.Params["disableHistory"].(bool); ok {
|
||||||
cfg.DisableHistory = v
|
cfg.DisableHistory = v
|
||||||
}
|
}
|
||||||
if v, ok := req.Params["disablePersist"].(bool); ok {
|
|
||||||
cfg.DisablePersist = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.SetConfig(cfg); err != nil {
|
if err := m.SetConfig(cfg); err != nil {
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
_ "golang.org/x/image/bmp"
|
_ "golang.org/x/image/bmp"
|
||||||
_ "golang.org/x/image/tiff"
|
_ "golang.org/x/image/tiff"
|
||||||
|
"hash/fnv"
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
|
|
||||||
@@ -69,6 +70,10 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
|||||||
}
|
}
|
||||||
m.db = db
|
m.db = db
|
||||||
|
|
||||||
|
if err := m.migrateHashes(); err != nil {
|
||||||
|
log.Errorf("Failed to migrate hashes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if config.ClearAtStartup {
|
if config.ClearAtStartup {
|
||||||
if err := m.clearHistoryInternal(); err != nil {
|
if err := m.clearHistoryInternal(); err != nil {
|
||||||
log.Errorf("Failed to clear history at startup: %v", err)
|
log.Errorf("Failed to clear history at startup: %v", err)
|
||||||
@@ -224,18 +229,10 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
m.offerMutex.RUnlock()
|
m.offerMutex.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
m.ownerLock.Lock()
|
|
||||||
wasOwner := m.isOwner
|
|
||||||
m.ownerLock.Unlock()
|
|
||||||
|
|
||||||
if offer == nil {
|
if offer == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if wasOwner {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
m.currentOffer = offer
|
m.currentOffer = offer
|
||||||
|
|
||||||
m.offerMutex.RLock()
|
m.offerMutex.RLock()
|
||||||
@@ -275,154 +272,62 @@ func (m *Manager) storeCurrentClipboard() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
allData := make(map[string][]byte)
|
preferredMime := m.selectMimeType(m.mimeTypes)
|
||||||
var orderedMimes []string
|
if preferredMime == "" {
|
||||||
|
preferredMime = m.mimeTypes[0]
|
||||||
for _, mime := range m.mimeTypes {
|
|
||||||
data, err := m.receiveData(offer, mime)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(data) == 0 || int64(len(data)) > cfg.MaxEntrySize {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
allData[mime] = data
|
|
||||||
orderedMimes = append(orderedMimes, mime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(allData) == 0 {
|
data, err := m.receiveData(offer, preferredMime)
|
||||||
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(data) == 0 || int64(len(data)) > cfg.MaxEntrySize {
|
||||||
preferredMime := m.selectMimeType(orderedMimes)
|
return
|
||||||
if preferredMime == "" {
|
|
||||||
preferredMime = orderedMimes[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data := allData[preferredMime]
|
|
||||||
if len(bytes.TrimSpace(data)) == 0 {
|
if len(bytes.TrimSpace(data)) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.DisableHistory && m.db != nil {
|
if !cfg.DisableHistory && m.db != nil {
|
||||||
entry := Entry{
|
m.storeClipboardEntry(data, preferredMime)
|
||||||
Data: data,
|
|
||||||
MimeType: preferredMime,
|
|
||||||
Size: len(data),
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
IsImage: m.isImageMimeType(preferredMime),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case entry.IsImage:
|
|
||||||
entry.Preview = m.imagePreview(data, preferredMime)
|
|
||||||
default:
|
|
||||||
entry.Preview = m.textPreview(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.storeEntry(entry); err != nil {
|
|
||||||
log.Errorf("Failed to store clipboard entry: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cfg.DisablePersist {
|
|
||||||
m.persistClipboard(orderedMimes, allData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.updateState()
|
m.updateState()
|
||||||
m.notifySubscribers()
|
m.notifySubscribers()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) persistClipboard(mimeTypes []string, data map[string][]byte) {
|
func (m *Manager) storeClipboardEntry(data []byte, mimeType string) {
|
||||||
m.persistMutex.Lock()
|
entry := Entry{
|
||||||
m.persistMimeTypes = mimeTypes
|
Data: data,
|
||||||
m.persistData = data
|
MimeType: mimeType,
|
||||||
m.persistMutex.Unlock()
|
Size: len(data),
|
||||||
|
Timestamp: time.Now(),
|
||||||
m.post(func() {
|
IsImage: m.isImageMimeType(mimeType),
|
||||||
m.takePersistOwnership()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) takePersistOwnership() {
|
|
||||||
if m.dataControlMgr == nil || m.dataDevice == nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.getConfig().DisablePersist {
|
switch {
|
||||||
return
|
case entry.IsImage:
|
||||||
|
entry.Preview = m.imagePreview(data, mimeType)
|
||||||
|
default:
|
||||||
|
entry.Preview = m.textPreview(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.persistMutex.RLock()
|
if err := m.storeEntry(entry); err != nil {
|
||||||
mimeTypes := m.persistMimeTypes
|
log.Errorf("Failed to store clipboard entry: %v", err)
|
||||||
m.persistMutex.RUnlock()
|
|
||||||
|
|
||||||
if len(mimeTypes) == 0 {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
|
|
||||||
|
|
||||||
source, err := dataMgr.CreateDataSource()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to create persist source: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mime := range mimeTypes {
|
|
||||||
if err := source.Offer(mime); err != nil {
|
|
||||||
log.Errorf("Failed to offer mime type %s: %v", mime, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
|
||||||
fd := e.Fd
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
|
|
||||||
m.persistMutex.RLock()
|
|
||||||
d := m.persistData[e.MimeType]
|
|
||||||
m.persistMutex.RUnlock()
|
|
||||||
|
|
||||||
if len(d) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
file := os.NewFile(uintptr(fd), "clipboard-pipe")
|
|
||||||
defer file.Close()
|
|
||||||
file.Write(d)
|
|
||||||
})
|
|
||||||
|
|
||||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
|
||||||
m.ownerLock.Lock()
|
|
||||||
m.isOwner = false
|
|
||||||
m.ownerLock.Unlock()
|
|
||||||
})
|
|
||||||
|
|
||||||
if m.currentSource != nil {
|
|
||||||
oldSource := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
|
||||||
oldSource.Destroy()
|
|
||||||
}
|
|
||||||
m.currentSource = source
|
|
||||||
|
|
||||||
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
|
|
||||||
if err := device.SetSelection(source); err != nil {
|
|
||||||
log.Errorf("Failed to set persist selection: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
m.ownerLock.Lock()
|
|
||||||
m.isOwner = true
|
|
||||||
m.ownerLock.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) storeEntry(entry Entry) error {
|
func (m *Manager) storeEntry(entry Entry) error {
|
||||||
if m.db == nil {
|
if m.db == nil {
|
||||||
return fmt.Errorf("database not available")
|
return fmt.Errorf("database not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.Hash = computeHash(entry.Data)
|
||||||
|
|
||||||
return m.db.Update(func(tx *bolt.Tx) error {
|
return m.db.Update(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket([]byte("clipboard"))
|
b := tx.Bucket([]byte("clipboard"))
|
||||||
|
|
||||||
if err := m.deduplicateInTx(b, entry.Data); err != nil {
|
if err := m.deduplicateInTx(b, entry.Hash); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,17 +351,14 @@ func (m *Manager) storeEntry(entry Entry) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) deduplicateInTx(b *bolt.Bucket, data []byte) error {
|
func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntry(v)
|
if extractHash(v) != hash {
|
||||||
if err != nil {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if bytes.Equal(entry.Data, data) {
|
if err := b.Delete(k); err != nil {
|
||||||
if err := b.Delete(k); err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -494,6 +396,7 @@ func encodeEntry(e Entry) ([]byte, error) {
|
|||||||
} else {
|
} else {
|
||||||
buf.WriteByte(0)
|
buf.WriteByte(0)
|
||||||
}
|
}
|
||||||
|
binary.Write(buf, binary.BigEndian, e.Hash)
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
@@ -533,6 +436,10 @@ func decodeEntry(data []byte) (Entry, error) {
|
|||||||
binary.Read(buf, binary.BigEndian, &isImage)
|
binary.Read(buf, binary.BigEndian, &isImage)
|
||||||
e.IsImage = isImage == 1
|
e.IsImage = isImage == 1
|
||||||
|
|
||||||
|
if buf.Len() >= 8 {
|
||||||
|
binary.Read(buf, binary.BigEndian, &e.Hash)
|
||||||
|
}
|
||||||
|
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,6 +449,19 @@ func itob(v uint64) []byte {
|
|||||||
return b
|
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 (m *Manager) selectMimeType(mimes []string) string {
|
func (m *Manager) selectMimeType(mimes []string) string {
|
||||||
preferredTypes := []string{
|
preferredTypes := []string{
|
||||||
"text/plain;charset=utf-8",
|
"text/plain;charset=utf-8",
|
||||||
@@ -1052,6 +972,79 @@ func (m *Manager) clearOldEntries(days int) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) migrateHashes() error {
|
||||||
|
if m.db == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsMigration bool
|
||||||
|
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("clipboard"))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
if extractHash(v) == 0 {
|
||||||
|
needsMigration = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !needsMigration {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Migrating clipboard entries to add hashes...")
|
||||||
|
|
||||||
|
return m.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("clipboard"))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var updates []struct {
|
||||||
|
key []byte
|
||||||
|
entry Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
entry, err := decodeEntry(v)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if entry.Hash != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry.Hash = computeHash(entry.Data)
|
||||||
|
keyCopy := make([]byte, len(k))
|
||||||
|
copy(keyCopy, k)
|
||||||
|
updates = append(updates, struct {
|
||||||
|
key []byte
|
||||||
|
entry Entry
|
||||||
|
}{keyCopy, entry})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range updates {
|
||||||
|
encoded, err := encodeEntry(u.entry)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := b.Put(u.key, encoded); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Migrated %d clipboard entries", len(updates))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) Search(params SearchParams) SearchResult {
|
func (m *Manager) Search(params SearchParams) SearchResult {
|
||||||
if m.db == nil {
|
if m.db == nil {
|
||||||
return SearchResult{}
|
return SearchResult{}
|
||||||
@@ -1212,35 +1205,12 @@ func (m *Manager) applyConfigChange(newCfg Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if newCfg.DisablePersist && !oldCfg.DisablePersist {
|
log.Infof("Clipboard config reloaded: disableHistory=%v", newCfg.DisableHistory)
|
||||||
log.Info("Clipboard persist disabled, releasing ownership")
|
|
||||||
m.releaseOwnership()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Clipboard config reloaded: disableHistory=%v disablePersist=%v",
|
|
||||||
newCfg.DisableHistory, newCfg.DisablePersist)
|
|
||||||
|
|
||||||
m.updateState()
|
m.updateState()
|
||||||
m.notifySubscribers()
|
m.notifySubscribers()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) releaseOwnership() {
|
|
||||||
m.ownerLock.Lock()
|
|
||||||
m.isOwner = false
|
|
||||||
m.ownerLock.Unlock()
|
|
||||||
|
|
||||||
m.persistMutex.Lock()
|
|
||||||
m.persistData = nil
|
|
||||||
m.persistMimeTypes = nil
|
|
||||||
m.persistMutex.Unlock()
|
|
||||||
|
|
||||||
if m.currentSource != nil {
|
|
||||||
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
|
||||||
source.Destroy()
|
|
||||||
m.currentSource = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) StoreData(data []byte, mimeType string) error {
|
func (m *Manager) StoreData(data []byte, mimeType string) error {
|
||||||
cfg := m.getConfig()
|
cfg := m.getConfig()
|
||||||
|
|
||||||
|
|||||||
@@ -289,81 +289,6 @@ func TestManager_ConcurrentOfferAccess(t *testing.T) {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_ConcurrentPersistAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
persistData: make(map[string][]byte),
|
|
||||||
persistMimeTypes: []string{},
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.persistMutex.RLock()
|
|
||||||
_ = m.persistData
|
|
||||||
_ = m.persistMimeTypes
|
|
||||||
m.persistMutex.RUnlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.persistMutex.Lock()
|
|
||||||
m.persistMimeTypes = []string{"text/plain", "text/html"}
|
|
||||||
m.persistData = map[string][]byte{
|
|
||||||
"text/plain": []byte("test"),
|
|
||||||
}
|
|
||||||
m.persistMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentOwnerAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.ownerLock.Lock()
|
|
||||||
_ = m.isOwner
|
|
||||||
m.ownerLock.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.ownerLock.Lock()
|
|
||||||
m.isOwner = j%2 == 0
|
|
||||||
m.ownerLock.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestItob(t *testing.T) {
|
func TestItob(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input uint64
|
input uint64
|
||||||
@@ -456,7 +381,6 @@ func TestDefaultConfig(t *testing.T) {
|
|||||||
assert.False(t, cfg.ClearAtStartup)
|
assert.False(t, cfg.ClearAtStartup)
|
||||||
assert.False(t, cfg.Disabled)
|
assert.False(t, cfg.Disabled)
|
||||||
assert.False(t, cfg.DisableHistory)
|
assert.False(t, cfg.DisableHistory)
|
||||||
assert.True(t, cfg.DisablePersist)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_PostDelegatesToWlContext(t *testing.T) {
|
func TestManager_PostDelegatesToWlContext(t *testing.T) {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ type Config struct {
|
|||||||
|
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
DisableHistory bool `json:"disableHistory"`
|
DisableHistory bool `json:"disableHistory"`
|
||||||
DisablePersist bool `json:"disablePersist"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() Config {
|
func DefaultConfig() Config {
|
||||||
@@ -30,7 +29,6 @@ func DefaultConfig() Config {
|
|||||||
MaxEntrySize: 5 * 1024 * 1024,
|
MaxEntrySize: 5 * 1024 * 1024,
|
||||||
AutoClearDays: 0,
|
AutoClearDays: 0,
|
||||||
ClearAtStartup: false,
|
ClearAtStartup: false,
|
||||||
DisablePersist: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +101,7 @@ type Entry struct {
|
|||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
IsImage bool `json:"isImage"`
|
IsImage bool `json:"isImage"`
|
||||||
|
Hash uint64 `json:"hash,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
@@ -134,12 +133,6 @@ type Manager struct {
|
|||||||
sourceMimeTypes []string
|
sourceMimeTypes []string
|
||||||
sourceMutex sync.RWMutex
|
sourceMutex sync.RWMutex
|
||||||
|
|
||||||
persistData map[string][]byte
|
|
||||||
persistMimeTypes []string
|
|
||||||
persistMutex sync.RWMutex
|
|
||||||
|
|
||||||
isOwner bool
|
|
||||||
ownerLock sync.Mutex
|
|
||||||
initialized bool
|
initialized bool
|
||||||
|
|
||||||
alive bool
|
alive bool
|
||||||
|
|||||||
@@ -204,9 +204,6 @@ func handleClipboardSetConfig(conn net.Conn, req models.Request) {
|
|||||||
if v, ok := req.Params["disableHistory"].(bool); ok {
|
if v, ok := req.Params["disableHistory"].(bool); ok {
|
||||||
cfg.DisableHistory = v
|
cfg.DisableHistory = v
|
||||||
}
|
}
|
||||||
if v, ok := req.Params["disablePersist"].(bool); ok {
|
|
||||||
cfg.DisablePersist = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clipboard.SaveConfig(cfg); err != nil {
|
if err := clipboard.SaveConfig(cfg); err != nil {
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
|||||||
@@ -257,16 +257,6 @@ Item {
|
|||||||
checked: root.config.disableHistory ?? false
|
checked: root.config.disableHistory ?? false
|
||||||
onToggled: checked => root.saveConfig("disableHistory", checked)
|
onToggled: checked => root.saveConfig("disableHistory", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsToggleRow {
|
|
||||||
tab: "clipboard"
|
|
||||||
tags: ["clipboard", "disable", "persist", "ownership"]
|
|
||||||
settingKey: "disablePersist"
|
|
||||||
text: I18n.tr("Disable Clipboard Ownership")
|
|
||||||
description: I18n.tr("Don't preserve clipboard when apps close")
|
|
||||||
checked: root.config.disablePersist ?? false
|
|
||||||
onToggled: checked => root.saveConfig("disablePersist", checked)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user