Commits

Paul Ruane committed ae46b46

Fixed bug in 'status' command where previous directory handle being left open during recursive operation.
Removed stat from 'tags' command which meant command failed if file had been deleted.
Improved 'untag' command so that it is possible to remove all tags with the '--all' option.
Added 'dupes' command.
Added 'stats' command.

  • Participants
  • Parent commits 6a608ab

Comments (0)

Files changed (9)

src/main/Makefile

 		main.go \
 		version.gen.go \
 		command.go \
+		commands/delete.go \
+		commands/dupes.go \
+		commands/export.go \
 		commands/help.go \
+		commands/merge.go \
 		commands/mount.go \
+		commands/rename.go \
+		commands/stats.go \
+		commands/status.go \
+		commands/tag.go \
+		commands/tags.go \
 		commands/unmount.go \
-		commands/tags.go \
-		commands/tag.go \
 		commands/untag.go \
-		commands/rename.go \
-		commands/merge.go \
-		commands/delete.go \
-		commands/export.go \
+		commands/version.go \
 		commands/vfs.go \
-		commands/status.go \
-		commands/version.go \
 		entities/tag.go \
 		entities/file.go \
 		entities/filetag.go \

src/main/commands/dupes.go

+/*
+Copyright 2011 Paul Ruane.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+package main
+*/
+
+package main
+
+import (
+	"errors"
+	"fmt"
+	"path/filepath"
+)
+
+type DupesCommand struct{}
+
+func (this DupesCommand) Name() string {
+	return "dupes"
+}
+
+func (this DupesCommand) Summary() string {
+	return "identifies any duplicate files"
+}
+
+func (this DupesCommand) Help() string {
+	return `  tmsu dupes [FILE]
+
+Identifies all files in the database that are exact duplicates of FILE.
+
+When FILE is omitted duplicates within the database are identified.`
+}
+
+func (this DupesCommand) Exec(args []string) error {
+    argCount := len(args)
+    if argCount > 1 { errors.New("Only a single file can be specified.") }
+
+    switch argCount {
+        case 0: return this.findDuplicates()
+        case 1: return this.findDuplicatesOf(args[0])
+    }
+
+	return nil
+}
+
+// implementation
+
+func (this DupesCommand) findDuplicates() error {
+    db, error := OpenDatabase(databasePath())
+    if error != nil { return error }
+    defer db.Close()
+
+    fileSets, error := db.DuplicateFiles()
+    if error != nil { return error }
+
+    for index, fileSet := range fileSets {
+        if index > 0 { fmt.Println() }
+
+        for _, file := range fileSet {
+            fmt.Println(file.Path)
+        }
+    }
+
+    return nil
+}
+
+func (this DupesCommand) findDuplicatesOf(path string) error {
+    db, error := OpenDatabase(databasePath())
+    if error != nil { return error }
+    defer db.Close()
+
+    fingerprint, error := Fingerprint(path)
+    if error != nil { return error }
+
+    files, error := db.FilesByFingerprint(fingerprint)
+    if error != nil { return error }
+
+    absPath, error := filepath.Abs(path)
+    if error != nil { return error }
+
+    for _, file := range files {
+        if file.Path == absPath { continue }
+
+        fmt.Println(file.Path)
+    }
+
+    return nil
+}

src/main/commands/stats.go

+/*
+Copyright 2011 Paul Ruane.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+package main
+*/
+
+package main
+
+import (
+    "fmt"
+)
+
+type StatsCommand struct {}
+
+func (this StatsCommand) Name() string {
+    return "stats"
+}
+
+func (this StatsCommand) Summary() string {
+    return "shows database statistics"
+}
+
+func (this StatsCommand) Help() string {
+    return `tmsu stats
+tmsu stats
+
+Shows the database statistics.`
+}
+
+func (this StatsCommand) Exec(args []string) error {
+    db, error := OpenDatabase(databasePath())
+    if error != nil { return error }
+    defer db.Close()
+
+    tagCount, error := db.TagCount()
+    if error != nil { return error }
+
+    fileCount, error := db.FileCount()
+    if error != nil { return error }
+
+    fileTagCount, error := db.FileTagCount()
+    if error != nil { return error }
+
+    fmt.Printf("Database Contents\n")
+
+    fmt.Printf(" Tags:             %v\n", tagCount)
+    fmt.Printf(" Files:            %v\n", fileCount)
+    fmt.Printf(" Taggings:         %v\n", fileTagCount)
+    fmt.Printf("   Avg. per file:  %v\n", fileTagCount / fileCount) 
+
+    return nil
+}

src/main/commands/status.go

 
 func (this StatusCommand) status(paths []string, tagged []string, untagged []string) ([]string, []string, error) {
     db, error := OpenDatabase(databasePath())
-    if error != nil {
-        return nil, nil, error
-    }
+    if error != nil { return nil, nil, error }
+    defer db.Close()
 
     return this.statusRecursive(db, paths, tagged, untagged)
 }
 func (this StatusCommand) statusRecursive(db *Database, paths []string, tagged []string, untagged []string) ([]string, []string, error) {
     for _, path := range paths {
         fileInfo, error := os.Lstat(path)
-        if error != nil {
-            return nil, nil, error
-        }
+        if error != nil { return nil, nil, error }
 
         if fileInfo.Mode() & os.ModeType == 0 {
             absPath, error := filepath.Abs(path)
-            if error != nil {
-                return nil, nil, error
-            }
+            if error != nil { return nil, nil, error }
 
             file, error := db.FileByPath(absPath)
-            if error != nil {
-                return nil, nil, error
-            }
+            if error != nil { return nil, nil, error }
 
             if file == nil {
                 untagged = append(untagged, absPath)
             }
         } else if fileInfo.IsDir() {
             file, error := os.Open(path)
-            if error != nil {
-                return nil, nil, error
-            }
-            defer file.Close()
+            if error != nil { return nil, nil, error }
 
             dirNames, error := file.Readdirnames(0)
-            if error != nil {
-                return nil, nil, error
-            }
+            if error != nil { return nil, nil, error }
+
+            error = file.Close()
+            if error != nil { return nil, nil, error }
 
             childPaths := make([]string, len(dirNames))
             for index, dirName := range dirNames {
             }
 
             tagged, untagged, error = this.statusRecursive(db, childPaths, tagged, untagged)
-            if error != nil {
-                return nil, nil, error
-            }
+            if error != nil { return nil, nil, error }
         }
     }
 

src/main/commands/tag.go

 	if error != nil { return nil, error }
 
 	if file == nil {
-		file, error = db.FileByFingerprint(fingerprint)
+		files, error := db.FilesByFingerprint(fingerprint)
 		if error != nil { return nil, error }
 
-		if file != nil {
+		if files != nil {
 			fmt.Printf("Warning: file is a duplicate of a previously tagged file.\n")
 		}
 

src/main/commands/tags.go

 	defer db.Close()
 
     if len(paths) == 1 {
-        fileInfo, error := os.Lstat(paths[0])
+        tags, error := this.tagsForPath(db, paths[0])
         if error != nil { return error }
+        if tags == nil { return nil }
 
-        if fileInfo.Mode() & os.ModeType == 0 {
-            tags, error := this.tagsForPath(db, paths[0])
-            if error != nil { return error }
-            if tags == nil { return nil }
+        for _, tag := range tags {
+            fmt.Println(tag.Name)
+        }
 
-            for _, tag := range tags {
-                fmt.Println(tag.Name)
-            }
-
-            return nil
-        }
+        return nil
     }
 
     return this.listTagsRecursive(db, paths)

src/main/commands/untag.go

 
 func (this UntagCommand) Help() string {
 	return `  tmsu untag FILE TAG...
-  tmsu untag FILE
+  tmsu untag --all FILE
 
-Disassociates the specified file FILE with the tag(s) specified.
+Disassociates FILE with the TAGs specified.
 
-If no tags are specified then the file will be stripped of all tags.`
+If the --all option is specified then the file will be stripped of all tags.`
 }
 
 func (this UntagCommand) Exec(args []string) error {
-	if len(args) < 2 {
-		return errors.New("File to untag and tags to remove must be specified.")
+	if len(args) < 1 {
+		return errors.New("No arguments specified.")
 	}
 
-	error := this.untagPath(args[0], args[1:])
-	if error != nil {
-		return error
-	}
+    if args[0] == "--all" {
+        if len(args) > 1 { return errors.New("Too many arguments.") }
+
+        error := this.removeFile(args[1])
+        if error != nil { return error }
+    } else {
+        if len(args) < 2 { return errors.New("Tags to remove must be specified.") }
+
+        error := this.untagFile(args[0], args[1:])
+        if error != nil { return error }
+    }
+
 
 	return nil
 }
 
 // implementation
 
-func (this UntagCommand) untagPath(path string, tagNames []string) error {
+func (this UntagCommand) removeFile(path string) error {
 	absPath, error := filepath.Abs(path)
 	if error != nil {
 		return error
 	defer db.Close()
 
 	file, error := db.FileByPath(absPath)
-	if error != nil {
-		return error
-	}
-	if file == nil {
-		return errors.New("File '" + path + "' is not tagged.")
-	}
+	if error != nil { return error }
+	if file == nil { return errors.New("File '" + path + "' is not tagged.") }
 
-	for _, tagName := range tagNames {
-		error = this.unapplyTag(db, path, file.Id, tagName)
-		if error != nil {
-			return error
-		}
-	}
+    error = db.RemoveFileTagsByFileId(file.Id)
+    if error != nil { return error }
+
+	error = db.RemoveFile(file.Id)
+    if error != nil { return error }
+
+    return nil
+}
+
+func (this UntagCommand) untagFile(path string, tagNames []string) error {
+	absPath, error := filepath.Abs(path)
+	if error != nil { return error }
+
+	db, error := OpenDatabase(databasePath())
+	if error != nil { return error }
+	defer db.Close()
+
+	file, error := db.FileByPath(absPath)
+	if error != nil { return error }
+	if file == nil { return errors.New("File '" + path + "' is not tagged.") }
+
+    for _, tagName := range tagNames {
+        error = this.unapplyTag(db, path, file.Id, tagName)
+        if error != nil { return error }
+    }
 
 	hasTags, error := db.AnyFileTagsForFile(file.Id)
-	if error != nil {
-		return error
-	}
+	if error != nil { return error }
 
 	if !hasTags {
-		db.RemoveFile(file.Id)
+        error := db.RemoveFile(file.Id)
+        if error != nil { return error }
 	}
 
 	return nil

src/main/database.go

 	return this.connection.Close()
 }
 
+func (this Database) TagCount() (uint, error) {
+	sql := `SELECT count(1)
+			FROM tag`
+
+	rows, error := this.connection.Query(sql)
+	if error != nil { return 0, error }
+	defer rows.Close()
+
+	if !rows.Next() { return 0, errors.New("Could not get tag count.") }
+	if rows.Err() != nil { return 0, error }
+
+	var count uint
+	error = rows.Scan(&count)
+	if error != nil { return 0, error }
+
+	return count, nil
+}
+
 func (this Database) Tags() ([]Tag, error) {
 	sql := `SELECT id, name
             FROM tag
 	return nil
 }
 
+func (this Database) FileCount() (uint, error) {
+	sql := `SELECT count(1)
+			FROM file`
+
+	rows, error := this.connection.Query(sql)
+	if error != nil { return 0, error }
+	defer rows.Close()
+
+	if !rows.Next() { return 0, errors.New("Could not get file count.") }
+	if rows.Err() != nil { return 0, error }
+
+	var count uint
+	error = rows.Scan(&count)
+	if error != nil { return 0, error }
+
+	return count, nil
+}
+
 func (this Database) Files() (*[]File, error) {
 	sql := `SELECT id, path, fingerprint
 	        FROM file`
 	return &File{id, path, fingerprint}, nil
 }
 
-func (this Database) FileByFingerprint(fingerprint string) (*File, error) {
+func (this Database) FilesByFingerprint(fingerprint string) ([]File, error) {
 	sql := `SELECT id, path
 	        FROM file
 	        WHERE fingerprint = ?`
 	if error != nil { return nil, error }
 	defer rows.Close()
 
-	if !rows.Next() { return nil, nil }
-	if rows.Err() != nil { return nil, error }
+	files := make([]File, 0, 10)
+	for rows.Next() {
+        if rows.Err() != nil { return nil, error }
 
-	var id uint
-	var path string
-	error = rows.Scan(&id, &path)
+		var fileId uint
+		var path string
+		error = rows.Scan(&fileId, &path)
+		if error != nil { return nil, error }
+
+		files = append(files, File{fileId, path, fingerprint})
+	}
+
+	return files, nil
+}
+
+func (this Database) DuplicateFiles() ([][]File, error) {
+    sql := `SELECT id, path, fingerprint
+            FROM file
+            WHERE fingerprint IN (SELECT fingerprint
+                                FROM file
+                                GROUP BY fingerprint
+                                HAVING count(1) > 1)
+            ORDER BY fingerprint`
+
+	rows, error := this.connection.Query(sql)
 	if error != nil { return nil, error }
+	defer rows.Close()
 
-	return &File{id, path, fingerprint}, nil
+    fileSets := make([][]File, 0, 10)
+    var fileSet []File
+    var previousFingerprint string
+
+	for rows.Next() {
+        if rows.Err() != nil { return nil, error }
+
+		var fileId uint
+		var path string
+		var fingerprint string
+		error = rows.Scan(&fileId, &path, &fingerprint)
+		if error != nil { return nil, error }
+
+	    if fingerprint != previousFingerprint {
+	        if fileSet != nil { fileSets = append(fileSets, fileSet) }
+            fileSet = make([]File, 0, 10)
+            previousFingerprint = fingerprint
+        }
+
+		fileSet = append(fileSet, File{fileId, path, fingerprint})
+	}
+
+    // ensure last file set is added
+    fileSets = append(fileSets, fileSet)
+
+	return fileSets, nil
 }
 
 func (this Database) AddFile(path string, fingerprint string) (*File, error) {
 	return nil
 }
 
+func (this Database) FileTagCount() (uint, error) {
+	sql := `SELECT count(1)
+			FROM file_tag`
+
+	rows, error := this.connection.Query(sql)
+	if error != nil { return 0, error }
+	defer rows.Close()
+
+	if !rows.Next() { return 0, errors.New("Could not get file-tag count.") }
+	if rows.Err() != nil { return 0, error }
+
+	var count uint
+	error = rows.Scan(&count)
+	if error != nil { return 0, error }
+
+	return count, nil
+}
+
 func (this Database) FileTags() ([]FileTag, error) {
 	sql := `SELECT id, file_id, tag_id
 	        FROM file_tag`
 
 func main() {
 	commandArray := []Command{
+		DeleteCommand{},
+		DupesCommand{},
+		ExportCommand{},
 		HelpCommand{},
+		MergeCommand{},
 		MountCommand{},
+		RenameCommand{},
+		StatsCommand{},
+		StatusCommand{},
+		TagCommand{},
+		TagsCommand{},
 		UnmountCommand{},
-		TagsCommand{},
-		TagCommand{},
 		UntagCommand{},
-		RenameCommand{},
-		MergeCommand{},
-		DeleteCommand{},
-		ExportCommand{},
+		VersionCommand{},
 		VfsCommand{},
-		StatusCommand{},
-		VersionCommand{},
 	}
 
 	commands = make(map[string]Command, len(commandArray))