mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-12 16:26:37 +00:00
Add label next to "Last file received" (fixes #1952)
This commit is contained in:
parent
198725216f
commit
12a3086a9e
@ -271,10 +271,12 @@
|
|||||||
<th><span class="glyphicon glyphicon-share-alt"></span> <span translate>Shared With</span></th>
|
<th><span class="glyphicon glyphicon-share-alt"></span> <span translate>Shared With</span></th>
|
||||||
<td class="text-right">{{sharesFolder(folder)}}</td>
|
<td class="text-right">{{sharesFolder(folder)}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!folder.readOnly && folderStats[folder.id].lastFile">
|
<tr ng-if="!folder.readOnly && folderStats[folder.id].lastFile && folderStats[folder.id].lastFile.filename">
|
||||||
<th><span class="glyphicon glyphicon-transfer"></span> <span translate>Last File Received</span></th>
|
<th><span class="glyphicon glyphicon-transfer"></span> <span translate>Last File Received</span></th>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span title="{{folderStats[folder.id].lastFile.filename}} @ {{folderStats[folder.id].lastFile.at | date:'yyyy-MM-dd HH:mm:ss'}}">
|
<span title="{{folderStats[folder.id].lastFile.filename}} @ {{folderStats[folder.id].lastFile.at | date:'yyyy-MM-dd HH:mm:ss'}}">
|
||||||
|
<span translate ng-if="!folderStats[folder.id].lastFile.deleted">Updated</span>
|
||||||
|
<span translate ng-if="folderStats[folder.id].lastFile.deleted">Deleted</span>
|
||||||
{{folderStats[folder.id].lastFile.filename | basename}}
|
{{folderStats[folder.id].lastFile.filename | basename}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
File diff suppressed because one or more lines are too long
@ -129,6 +129,28 @@ func (n NamespacedKV) Bytes(key string) ([]byte, bool) {
|
|||||||
return valBs, true
|
return valBs, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutBool stores a new boolean. Any existing value (even if of another type)
|
||||||
|
// is overwritten.
|
||||||
|
func (n *NamespacedKV) PutBool(key string, val bool) {
|
||||||
|
keyBs := append(n.prefix, []byte(key)...)
|
||||||
|
if val {
|
||||||
|
n.db.Put(keyBs, []byte{0x0}, nil)
|
||||||
|
} else {
|
||||||
|
n.db.Put(keyBs, []byte{0x1}, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool returns the stored value as a boolean and a boolean that
|
||||||
|
// is false if no value was stored at the key.
|
||||||
|
func (n NamespacedKV) Bool(key string) (bool, bool) {
|
||||||
|
keyBs := append(n.prefix, []byte(key)...)
|
||||||
|
valBs, err := n.db.Get(keyBs, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
return valBs[0] == 0x0, true
|
||||||
|
}
|
||||||
|
|
||||||
// Delete deletes the specified key. It is allowed to delete a nonexistent
|
// Delete deletes the specified key. It is allowed to delete a nonexistent
|
||||||
// key.
|
// key.
|
||||||
func (n NamespacedKV) Delete(key string) {
|
func (n NamespacedKV) Delete(key string) {
|
||||||
|
@ -1025,8 +1025,8 @@ func (m *Model) folderStatRef(folder string) *stats.FolderStatisticsReference {
|
|||||||
return sr
|
return sr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) receivedFile(folder, filename string) {
|
func (m *Model) receivedFile(folder string, file protocol.FileInfo) {
|
||||||
m.folderStatRef(folder).ReceivedFile(filename)
|
m.folderStatRef(folder).ReceivedFile(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher) {
|
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher) {
|
||||||
|
@ -54,6 +54,19 @@ var (
|
|||||||
errNoDevice = errors.New("no available source device")
|
errNoDevice = errors.New("no available source device")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dbUpdateHandleDir = iota
|
||||||
|
dbUpdateDeleteDir
|
||||||
|
dbUpdateHandleFile
|
||||||
|
dbUpdateDeleteFile
|
||||||
|
dbUpdateShortcutFile
|
||||||
|
)
|
||||||
|
|
||||||
|
type dbUpdateJob struct {
|
||||||
|
file protocol.FileInfo
|
||||||
|
jobType int
|
||||||
|
}
|
||||||
|
|
||||||
type rwFolder struct {
|
type rwFolder struct {
|
||||||
stateTracker
|
stateTracker
|
||||||
|
|
||||||
@ -73,7 +86,7 @@ type rwFolder struct {
|
|||||||
|
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
queue *jobQueue
|
queue *jobQueue
|
||||||
dbUpdates chan protocol.FileInfo
|
dbUpdates chan dbUpdateJob
|
||||||
scanTimer *time.Timer
|
scanTimer *time.Timer
|
||||||
pullTimer *time.Timer
|
pullTimer *time.Timer
|
||||||
delayScan chan time.Duration
|
delayScan chan time.Duration
|
||||||
@ -326,7 +339,7 @@ func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
|||||||
l.Debugln(p, "c", p.copiers, "p", p.pullers)
|
l.Debugln(p, "c", p.copiers, "p", p.pullers)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.dbUpdates = make(chan protocol.FileInfo)
|
p.dbUpdates = make(chan dbUpdateJob)
|
||||||
updateWg.Add(1)
|
updateWg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
// dbUpdaterRoutine finishes when p.dbUpdates is closed
|
// dbUpdaterRoutine finishes when p.dbUpdates is closed
|
||||||
@ -583,7 +596,7 @@ func (p *rwFolder) handleDir(file protocol.FileInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = osutil.InWritableDir(mkdir, realName); err == nil {
|
if err = osutil.InWritableDir(mkdir, realName); err == nil {
|
||||||
p.dbUpdates <- file
|
p.dbUpdates <- dbUpdateJob{file, dbUpdateHandleDir}
|
||||||
} else {
|
} else {
|
||||||
l.Infof("Puller (folder %q, dir %q): %v", p.folder, file.Name, err)
|
l.Infof("Puller (folder %q, dir %q): %v", p.folder, file.Name, err)
|
||||||
}
|
}
|
||||||
@ -600,9 +613,9 @@ func (p *rwFolder) handleDir(file protocol.FileInfo) {
|
|||||||
// It's OK to change mode bits on stuff within non-writable directories.
|
// It's OK to change mode bits on stuff within non-writable directories.
|
||||||
|
|
||||||
if p.ignorePermissions(file) {
|
if p.ignorePermissions(file) {
|
||||||
p.dbUpdates <- file
|
p.dbUpdates <- dbUpdateJob{file, dbUpdateHandleDir}
|
||||||
} else if err := os.Chmod(realName, mode); err == nil {
|
} else if err := os.Chmod(realName, mode); err == nil {
|
||||||
p.dbUpdates <- file
|
p.dbUpdates <- dbUpdateJob{file, dbUpdateHandleDir}
|
||||||
} else {
|
} else {
|
||||||
l.Infof("Puller (folder %q, dir %q): %v", p.folder, file.Name, err)
|
l.Infof("Puller (folder %q, dir %q): %v", p.folder, file.Name, err)
|
||||||
}
|
}
|
||||||
@ -642,13 +655,13 @@ func (p *rwFolder) deleteDir(file protocol.FileInfo) {
|
|||||||
err = osutil.InWritableDir(osutil.Remove, realName)
|
err = osutil.InWritableDir(osutil.Remove, realName)
|
||||||
if err == nil || os.IsNotExist(err) {
|
if err == nil || os.IsNotExist(err) {
|
||||||
// It was removed or it doesn't exist to start with
|
// It was removed or it doesn't exist to start with
|
||||||
p.dbUpdates <- file
|
p.dbUpdates <- dbUpdateJob{file, dbUpdateDeleteDir}
|
||||||
} else if _, serr := os.Lstat(realName); serr != nil && !os.IsPermission(serr) {
|
} else if _, serr := os.Lstat(realName); serr != nil && !os.IsPermission(serr) {
|
||||||
// We get an error just looking at the directory, and it's not a
|
// We get an error just looking at the directory, and it's not a
|
||||||
// permission problem. Lets assume the error is in fact some variant
|
// permission problem. Lets assume the error is in fact some variant
|
||||||
// of "file does not exist" (possibly expressed as some parent being a
|
// of "file does not exist" (possibly expressed as some parent being a
|
||||||
// file and not a directory etc) and that the delete is handled.
|
// file and not a directory etc) and that the delete is handled.
|
||||||
p.dbUpdates <- file
|
p.dbUpdates <- dbUpdateJob{file, dbUpdateDeleteDir}
|
||||||
} else {
|
} else {
|
||||||
l.Infof("Puller (folder %q, dir %q): delete: %v", p.folder, file.Name, err)
|
l.Infof("Puller (folder %q, dir %q): delete: %v", p.folder, file.Name, err)
|
||||||
}
|
}
|
||||||
@ -690,13 +703,13 @@ func (p *rwFolder) deleteFile(file protocol.FileInfo) {
|
|||||||
|
|
||||||
if err == nil || os.IsNotExist(err) {
|
if err == nil || os.IsNotExist(err) {
|
||||||
// It was removed or it doesn't exist to start with
|
// It was removed or it doesn't exist to start with
|
||||||
p.dbUpdates <- file
|
p.dbUpdates <- dbUpdateJob{file, dbUpdateDeleteFile}
|
||||||
} else if _, serr := os.Lstat(realName); serr != nil && !os.IsPermission(serr) {
|
} else if _, serr := os.Lstat(realName); serr != nil && !os.IsPermission(serr) {
|
||||||
// We get an error just looking at the file, and it's not a permission
|
// We get an error just looking at the file, and it's not a permission
|
||||||
// problem. Lets assume the error is in fact some variant of "file
|
// problem. Lets assume the error is in fact some variant of "file
|
||||||
// does not exist" (possibly expressed as some parent being a file and
|
// does not exist" (possibly expressed as some parent being a file and
|
||||||
// not a directory etc) and that the delete is handled.
|
// not a directory etc) and that the delete is handled.
|
||||||
p.dbUpdates <- file
|
p.dbUpdates <- dbUpdateJob{file, dbUpdateDeleteFile}
|
||||||
} else {
|
} else {
|
||||||
l.Infof("Puller (folder %q, file %q): delete: %v", p.folder, file.Name, err)
|
l.Infof("Puller (folder %q, file %q): delete: %v", p.folder, file.Name, err)
|
||||||
}
|
}
|
||||||
@ -756,13 +769,15 @@ func (p *rwFolder) renameFile(source, target protocol.FileInfo) {
|
|||||||
// of the source and the creation of the target. Fix-up the metadata,
|
// of the source and the creation of the target. Fix-up the metadata,
|
||||||
// and update the local index of the target file.
|
// and update the local index of the target file.
|
||||||
|
|
||||||
p.dbUpdates <- source
|
p.dbUpdates <- dbUpdateJob{source, dbUpdateDeleteFile}
|
||||||
|
|
||||||
err = p.shortcutFile(target)
|
err = p.shortcutFile(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Infof("Puller (folder %q, file %q): rename from %q metadata: %v", p.folder, target.Name, source.Name, err)
|
l.Infof("Puller (folder %q, file %q): rename from %q metadata: %v", p.folder, target.Name, source.Name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.dbUpdates <- dbUpdateJob{target, dbUpdateHandleFile}
|
||||||
} else {
|
} else {
|
||||||
// We failed the rename so we have a source file that we still need to
|
// We failed the rename so we have a source file that we still need to
|
||||||
// get rid of. Attempt to delete it instead so that we make *some*
|
// get rid of. Attempt to delete it instead so that we make *some*
|
||||||
@ -774,7 +789,7 @@ func (p *rwFolder) renameFile(source, target protocol.FileInfo) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.dbUpdates <- source
|
p.dbUpdates <- dbUpdateJob{source, dbUpdateDeleteFile}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -848,6 +863,11 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
|||||||
"type": "file",
|
"type": "file",
|
||||||
"action": "metadata",
|
"action": "metadata",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
p.dbUpdates <- dbUpdateJob{file, dbUpdateShortcutFile}
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -954,16 +974,13 @@ func (p *rwFolder) shortcutFile(file protocol.FileInfo) error {
|
|||||||
file.Version = file.Version.Merge(cur.Version)
|
file.Version = file.Version.Merge(cur.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.dbUpdates <- file
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortcutSymlink changes the symlinks type if necessary.
|
// shortcutSymlink changes the symlinks type if necessary.
|
||||||
func (p *rwFolder) shortcutSymlink(file protocol.FileInfo) (err error) {
|
func (p *rwFolder) shortcutSymlink(file protocol.FileInfo) (err error) {
|
||||||
err = symlinks.ChangeType(filepath.Join(p.dir, file.Name), file.Flags)
|
err = symlinks.ChangeType(filepath.Join(p.dir, file.Name), file.Flags)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
p.dbUpdates <- file
|
|
||||||
} else {
|
|
||||||
l.Infof("Puller (folder %q, file %q): symlink shortcut: %v", p.folder, file.Name, err)
|
l.Infof("Puller (folder %q, file %q): symlink shortcut: %v", p.folder, file.Name, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -1183,7 +1200,7 @@ func (p *rwFolder) performFinish(state *sharedPullerState) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Record the updated file in the index
|
// Record the updated file in the index
|
||||||
p.dbUpdates <- state.file
|
p.dbUpdates <- dbUpdateJob{state.file, dbUpdateHandleFile}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1239,39 +1256,63 @@ func (p *rwFolder) dbUpdaterRoutine() {
|
|||||||
maxBatchTime = 2 * time.Second
|
maxBatchTime = 2 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
batch := make([]protocol.FileInfo, 0, maxBatchSize)
|
batch := make([]dbUpdateJob, 0, maxBatchSize)
|
||||||
|
files := make([]protocol.FileInfo, 0, maxBatchSize)
|
||||||
tick := time.NewTicker(maxBatchTime)
|
tick := time.NewTicker(maxBatchTime)
|
||||||
defer tick.Stop()
|
defer tick.Stop()
|
||||||
|
|
||||||
|
handleBatch := func() {
|
||||||
|
found := false
|
||||||
|
var lastFile protocol.FileInfo
|
||||||
|
|
||||||
|
for _, job := range batch {
|
||||||
|
files = append(files, job.file)
|
||||||
|
if job.file.IsInvalid() || (job.file.IsDirectory() && !job.file.IsSymlink()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if job.jobType&(dbUpdateHandleFile|dbUpdateDeleteFile) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
found = true
|
||||||
|
lastFile = job.file
|
||||||
|
}
|
||||||
|
|
||||||
|
p.model.updateLocals(p.folder, files)
|
||||||
|
|
||||||
|
if found {
|
||||||
|
p.model.receivedFile(p.folder, lastFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch = batch[:0]
|
||||||
|
files = files[:0]
|
||||||
|
}
|
||||||
|
|
||||||
loop:
|
loop:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case file, ok := <-p.dbUpdates:
|
case job, ok := <-p.dbUpdates:
|
||||||
if !ok {
|
if !ok {
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
|
|
||||||
file.LocalVersion = 0
|
job.file.LocalVersion = 0
|
||||||
batch = append(batch, file)
|
batch = append(batch, job)
|
||||||
|
|
||||||
if len(batch) == maxBatchSize {
|
if len(batch) == maxBatchSize {
|
||||||
p.model.updateLocals(p.folder, batch)
|
handleBatch()
|
||||||
p.model.receivedFile(p.folder, batch[len(batch)-1].Name)
|
|
||||||
batch = batch[:0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-tick.C:
|
case <-tick.C:
|
||||||
if len(batch) > 0 {
|
if len(batch) > 0 {
|
||||||
p.model.updateLocals(p.folder, batch)
|
handleBatch()
|
||||||
p.model.receivedFile(p.folder, batch[len(batch)-1].Name)
|
|
||||||
batch = batch[:0]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(batch) > 0 {
|
if len(batch) > 0 {
|
||||||
p.model.updateLocals(p.folder, batch)
|
handleBatch()
|
||||||
p.model.receivedFile(p.folder, batch[len(batch)-1].Name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@ package stats
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/protocol"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/internal/db"
|
"github.com/syncthing/syncthing/internal/db"
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
)
|
)
|
||||||
@ -25,6 +27,7 @@ type FolderStatisticsReference struct {
|
|||||||
type LastFile struct {
|
type LastFile struct {
|
||||||
At time.Time `json:"at"`
|
At time.Time `json:"at"`
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
|
Deleted bool `json:"deleted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFolderStatisticsReference(ldb *leveldb.DB, folder string) *FolderStatisticsReference {
|
func NewFolderStatisticsReference(ldb *leveldb.DB, folder string) *FolderStatisticsReference {
|
||||||
@ -44,18 +47,21 @@ func (s *FolderStatisticsReference) GetLastFile() LastFile {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return LastFile{}
|
return LastFile{}
|
||||||
}
|
}
|
||||||
|
deleted, ok := s.ns.Bool("lastFileDeleted")
|
||||||
return LastFile{
|
return LastFile{
|
||||||
At: at,
|
At: at,
|
||||||
Filename: file,
|
Filename: file,
|
||||||
|
Deleted: deleted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FolderStatisticsReference) ReceivedFile(filename string) {
|
func (s *FolderStatisticsReference) ReceivedFile(file protocol.FileInfo) {
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugln("stats.FolderStatisticsReference.ReceivedFile:", s.folder, filename)
|
l.Debugln("stats.FolderStatisticsReference.ReceivedFile:", s.folder, file)
|
||||||
}
|
}
|
||||||
s.ns.PutTime("lastFileAt", time.Now())
|
s.ns.PutTime("lastFileAt", time.Now())
|
||||||
s.ns.PutString("lastFileName", filename)
|
s.ns.PutString("lastFileName", file.Name)
|
||||||
|
s.ns.PutBool("lastFileDeleted", file.IsDeleted())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FolderStatisticsReference) GetStatistics() FolderStatistics {
|
func (s *FolderStatisticsReference) GetStatistics() FolderStatistics {
|
||||||
|
Loading…
Reference in New Issue
Block a user