Add label next to "Last file received" (fixes #1952)

This commit is contained in:
Audrius Butkevicius 2015-06-16 12:12:34 +01:00
parent 198725216f
commit 12a3086a9e
6 changed files with 107 additions and 36 deletions

View File

@ -271,10 +271,12 @@
<th><span class="glyphicon glyphicon-share-alt"></span>&nbsp;<span translate>Shared With</span></th> <th><span class="glyphicon glyphicon-share-alt"></span>&nbsp;<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>&nbsp;<span translate>Last File Received</span></th> <th><span class="glyphicon glyphicon-transfer"></span>&nbsp;<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

View File

@ -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) {

View File

@ -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) {

View File

@ -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)
} }
} }

View File

@ -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 {