コード例 #1
0
ファイル: database.go プロジェクト: Konubinix/tmsu
// Opens the database at the specified path
func OpenAt(path string) (*Database, error) {
	log.Infof(2, "opening database at '%v'.", path)

	_, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			log.Warnf("creating database at '%v'.", path)
		} else {
			log.Warnf("could not stat database: %v", err)
		}
	}

	connection, err := sql.Open("sqlite3", path)
	if err != nil {
		return nil, fmt.Errorf("could not open database: %v", err)
	}

	transaction, err := connection.Begin()
	if err != nil {
		return nil, fmt.Errorf("could not begin transaciton: %v", err)
	}

	database := &Database{connection, transaction}

	if err := database.CreateSchema(); err != nil {
		return nil, errors.New("could not create database schema: " + err.Error())
	}

	return database, nil
}
コード例 #2
0
ファイル: tag.go プロジェクト: Konubinix/tmsu
func tagFrom(fromPath string, paths []string, recursive bool) error {
	store, err := storage.Open()
	if err != nil {
		return fmt.Errorf("could not open storage: %v", err)
	}
	defer store.Close()
	defer store.Commit()

	fingerprintAlgorithmSetting, err := store.Setting("fingerprintAlgorithm")
	if err != nil {
		return fmt.Errorf("could not retrieve fingerprint algorithm: %v", err)
	}

	file, err := store.FileByPath(fromPath)
	if err != nil {
		return fmt.Errorf("%v: could not retrieve file: %v", fromPath, err)
	}
	if file == nil {
		return fmt.Errorf("%v: path is not tagged")
	}

	fileTags, err := store.FileTagsByFileId(file.Id)
	if err != nil {
		return fmt.Errorf("%v: could not retrieve filetags: %v", fromPath, err)
	}

	tagValuePairs := make([]TagValuePair, len(fileTags))
	for index, fileTag := range fileTags {
		tagValuePairs[index] = TagValuePair{fileTag.TagId, fileTag.ValueId}
	}

	wereErrors := false
	for _, path := range paths {
		if err := tagPath(store, path, tagValuePairs, recursive, fingerprintAlgorithmSetting.Value); err != nil {
			switch {
			case os.IsPermission(err):
				log.Warnf("%v: permisison denied", path)
				wereErrors = true
			case os.IsNotExist(err):
				log.Warnf("%v: no such file", path)
				wereErrors = true
			default:
				return fmt.Errorf("%v: could not stat file: %v", path, err)
			}
		}
	}

	if wereErrors {
		return blankError
	}

	return nil
}
コード例 #3
0
ファイル: status.go プロジェクト: Konubinix/tmsu
func statusCheckFile(file *entities.File, report *StatusReport) error {
	relPath := path.Rel(file.Path())

	log.Infof(2, "%v: checking file status.", file.Path())

	stat, err := os.Stat(file.Path())
	if err != nil {
		switch {
		case os.IsNotExist(err):
			log.Infof(2, "%v: file is missing.", file.Path())

			report.AddRow(Row{relPath, MISSING})
			return nil
		case os.IsPermission(err):
			log.Warnf("%v: permission denied.", file.Path())
		case strings.Contains(err.Error(), "not a directory"):
			report.AddRow(Row{relPath, MISSING})
			return nil
		default:
			return fmt.Errorf("%v: could not stat: %v", file.Path(), err)
		}
	} else {
		if stat.Size() != file.Size || stat.ModTime().UTC() != file.ModTime {
			log.Infof(2, "%v: file is modified.", file.Path())

			report.AddRow(Row{relPath, MODIFIED})
		} else {
			log.Infof(2, "%v: file is unchanged.", file.Path())

			report.AddRow(Row{relPath, TAGGED})
		}
	}

	return nil
}
コード例 #4
0
ファイル: tag.go プロジェクト: Konubinix/tmsu
func createTags(names []string) error {
	store, err := storage.Open()
	if err != nil {
		return fmt.Errorf("could not open storage: %v", err)
	}
	defer store.Close()
	defer store.Commit()

	tags, err := store.TagsByNames(names)
	if err != nil {
		return fmt.Errorf("could not retrieve tags %v: %v", names, err)
	}

	wereErrors := false
	for _, tag := range tags {
		log.Warnf("tag '%v' already exists", tag.Name)
		wereErrors = true
	}

	if wereErrors {
		return blankError
	}

	for _, name := range names {
		_, err := store.AddTag(name)
		if err != nil {
			return fmt.Errorf("could not add tag '%v': %v", name, err)
		}
	}

	return nil
}
コード例 #5
0
ファイル: untag.go プロジェクト: Konubinix/tmsu
func untagPathsAll(paths []string, recursive bool) error {
	store, err := storage.Open()
	if err != nil {
		return fmt.Errorf("could not open storage: %v", err)
	}
	defer store.Close()
	defer store.Commit()

	wereErrors := false
	for _, path := range paths {
		absPath, err := filepath.Abs(path)
		if err != nil {
			return fmt.Errorf("%v: could not get absolute path: %v", path, err)
		}

		file, err := store.FileByPath(absPath)
		if err != nil {
			return fmt.Errorf("%v: could not retrieve file: %v", path, err)
		}
		if file == nil {
			log.Warnf("%v: file is not tagged.", path)
			wereErrors = true
			continue
		}

		log.Infof(2, "%v: removing all tags.", file.Path())

		if err := store.DeleteFileTagsByFileId(file.Id); err != nil {
			return fmt.Errorf("%v: could not remove file's tags: %v", file.Path(), err)
		}

		if recursive {
			childFiles, err := store.FilesByDirectory(file.Path())
			if err != nil {
				return fmt.Errorf("%v: could not retrieve files for directory: %v", file.Path())
			}

			for _, childFile := range childFiles {
				if err := store.DeleteFileTagsByFileId(childFile.Id); err != nil {
					return fmt.Errorf("%v: could not remove file's tags: %v", childFile.Path(), err)
				}
			}
		}
	}

	if wereErrors {
		return blankError
	}

	return nil
}
コード例 #6
0
ファイル: status.go プロジェクト: Konubinix/tmsu
func findNewFiles(searchPath string, report *StatusReport, dirOnly bool) error {
	log.Infof(2, "%v: finding new files.", searchPath)

	relPath := path.Rel(searchPath)

	if !report.ContainsRow(relPath) {
		report.AddRow(Row{relPath, UNTAGGED})
	}

	absPath, err := filepath.Abs(searchPath)
	if err != nil {
		return fmt.Errorf("%v: could not get absolute path: %v", searchPath, err)
	}

	stat, err := os.Stat(absPath)
	if err != nil {
		switch {
		case os.IsNotExist(err):
			return nil
		case os.IsPermission(err):
			log.Warnf("%v: permission denied.", searchPath)
			return nil
		default:
			return fmt.Errorf("%v: could not stat: %v", searchPath, err)
		}
	}

	if !dirOnly && stat.IsDir() {
		dir, err := os.Open(absPath)
		if err != nil {
			return fmt.Errorf("%v: could not open file: %v", searchPath, err)
		}

		dirNames, err := dir.Readdirnames(0)
		if err != nil {
			return fmt.Errorf("%v: could not read directory listing: %v", searchPath, err)
		}

		for _, dirName := range dirNames {
			dirPath := filepath.Join(searchPath, dirName)
			err = findNewFiles(dirPath, report, dirOnly)
			if err != nil {
				return err
			}
		}
	}

	return nil
}
コード例 #7
0
ファイル: files.go プロジェクト: Konubinix/tmsu
func listFilesForQuery(queryText string, dirOnly, fileOnly, topOnly, leafOnly, recursive, print0, showCount, onePerLine bool) error {
	if queryText == "" {
		return fmt.Errorf("query must be specified. Use --all to show all files.")
	}

	store, err := storage.Open()
	if err != nil {
		return fmt.Errorf("could not open storage: %v", err)
	}
	defer store.Close()

	log.Info(2, "parsing query")

	expression, err := query.Parse(queryText)
	if err != nil {
		return err
	}

	log.Info(2, "checking tag names")

	wereErrors := false

	tagNames := query.TagNames(expression)
	tags, err := store.TagsByNames(tagNames)
	for _, tagName := range tagNames {
		if !tags.ContainsName(tagName) {
			log.Warnf("no such tag '%v'.", tagName)
			wereErrors = true
			continue
		}
	}

	if wereErrors {
		return blankError
	}

	log.Info(2, "querying database")

	files, err := store.QueryFiles(expression)
	if err != nil {
		return fmt.Errorf("could not query files: %v", err)
	}

	if err = listFiles(files, dirOnly, fileOnly, topOnly, leafOnly, recursive, print0, showCount, onePerLine); err != nil {
		return err
	}

	return nil
}
コード例 #8
0
ファイル: values.go プロジェクト: Konubinix/tmsu
func listValuesForTags(store *storage.Storage, tagNames []string, showCount, onePerLine bool) error {
	wereErrors := false
	for _, tagName := range tagNames {
		tag, err := store.TagByName(tagName)
		if err != nil {
			return fmt.Errorf("could not retrieve tag '%v': %v", tagName, err)
		}
		if tag == nil {
			log.Warnf("no such tag, '%v'.", tagName)
			wereErrors = true
			continue
		}

		log.Infof(2, "retrieving values for tag '%v'.", tagName)

		values, err := store.ValuesByTag(tag.Id)
		if err != nil {
			return fmt.Errorf("could not retrieve values for tag '%v': %v", tagName, err)
		}

		if showCount {
			fmt.Printf("%v: %v\n", tagName, len(values))
		} else {
			if onePerLine {
				fmt.Println(tagName)
				for _, value := range values {
					fmt.Println(value.Name)
				}
				fmt.Println()
			} else {
				valueNames := make([]string, len(values))
				for index, value := range values {
					valueNames[index] = value.Name
				}

				fmt.Printf("%v: %v\n", tagName, strings.Join(valueNames, " "))
			}
		}
	}

	if wereErrors {
		return blankError
	}

	return nil
}
コード例 #9
0
ファイル: tag.go プロジェクト: Konubinix/tmsu
func getOrCreateTag(store *storage.Storage, tagName string) (*entities.Tag, error) {
	tag, err := store.TagByName(tagName)
	if err != nil {
		return nil, fmt.Errorf("could not look up tag '%v': %v", tagName, err)
	}

	if tag == nil {
		tag, err = store.AddTag(tagName)
		if err != nil {
			return nil, fmt.Errorf("could not create tag '%v': %v", tagName, err)
		}

		log.Warnf("New tag '%v'.", tagName)
	}

	return tag, nil
}
コード例 #10
0
ファイル: copy.go プロジェクト: Konubinix/tmsu
func copyExec(options Options, args []string) error {
	store, err := storage.Open()
	if err != nil {
		return fmt.Errorf("could not open storage: %v", err)
	}
	defer store.Close()
	defer store.Commit()

	sourceTagName := args[0]
	destTagNames := args[1:]

	sourceTag, err := store.Db.TagByName(sourceTagName)
	if err != nil {
		return fmt.Errorf("could not retrieve tag '%v': %v", sourceTagName, err)
	}
	if sourceTag == nil {
		return fmt.Errorf("no such tag '%v'.", sourceTagName)
	}

	wereErrors := false
	for _, destTagName := range destTagNames {
		destTag, err := store.Db.TagByName(destTagName)
		if err != nil {
			return fmt.Errorf("could not retrieve tag '%v': %v", destTagName, err)
		}
		if destTag != nil {
			log.Warnf("a tag with name '%v' already exists.", destTagName)
			wereErrors = true
			continue
		}

		log.Infof(2, "copying tag '%v' to '%v'.", sourceTagName, destTagName)

		if _, err = store.CopyTag(sourceTag.Id, destTagName); err != nil {
			return fmt.Errorf("could not copy tag '%v' to '%v': %v", sourceTagName, destTagName, err)
		}
	}

	if wereErrors {
		return blankError
	}

	return nil
}
コード例 #11
0
ファイル: path.go プロジェクト: Konubinix/tmsu
func enumerate(path string, files []FileSystemFile) ([]FileSystemFile, error) {
	stat, err := os.Stat(path)
	if err != nil {
		switch {
		case os.IsNotExist(err):
			return files, nil
		case os.IsPermission(err):
			log.Warnf("%v: permission denied", path)
			return files, nil
		default:
			return nil, fmt.Errorf("%v: could not stat: %v", path, err)
		}
	}

	files = append(files, FileSystemFile{path, stat.IsDir()})

	if stat.IsDir() {
		dir, err := os.Open(path)
		if err != nil {
			return nil, fmt.Errorf("%v: could not open directory: %v", path, err)
		}

		names, err := dir.Readdirnames(0)
		dir.Close()
		if err != nil {
			return nil, fmt.Errorf("%v: could not read directory entries: %v", path, err)
		}

		for _, name := range names {
			childPath := filepath.Join(path, name)
			files, err = enumerate(childPath, files)
			if err != nil {
				return nil, err
			}
		}
	}

	return files, nil
}
コード例 #12
0
ファイル: delete.go プロジェクト: Konubinix/tmsu
func deleteExec(options Options, args []string) error {
	if len(args) == 0 {
		return fmt.Errorf("no tags to delete specified.")
	}

	store, err := storage.Open()
	if err != nil {
		return fmt.Errorf("could not open storage: %v", err)
	}
	defer store.Close()
	defer store.Commit()

	wereErrors := false
	for _, tagName := range args {
		tag, err := store.TagByName(tagName)
		if err != nil {
			return fmt.Errorf("could not retrieve tag '%v': %v", tagName, err)
		}
		if tag == nil {
			log.Warnf("no such tag '%v'.", tagName)
			wereErrors = true
			continue
		}

		err = store.DeleteTag(tag.Id)
		if err != nil {
			return fmt.Errorf("could not delete tag '%v': %v", tagName, err)
		}
	}

	if wereErrors {
		return blankError
	}

	return nil
}
コード例 #13
0
ファイル: repair.go プロジェクト: Konubinix/tmsu
func enumerateFileSystemPath(files fileInfoMap, path string) error {
	stat, err := os.Stat(path)
	if err != nil {
		switch {
		case os.IsPermission(err):
			log.Warnf("%v: permission denied", path)
			return nil
		case os.IsNotExist(err):
			return nil
		default:
			return fmt.Errorf("%v: could not stat file: %v", path, err)
		}
	}

	files[path] = stat

	if stat.IsDir() {
		file, err := os.Open(path)
		if err != nil {
			return fmt.Errorf("%v: could not open file: %v", path, err)
		}

		childFilenames, err := file.Readdirnames(0)
		file.Close()
		if err != nil {
			return fmt.Errorf("%v: could not read directory: %v", file.Name(), err)
		}

		for _, childFilename := range childFilenames {
			childPath := filepath.Join(path, childFilename)
			enumerateFileSystemPath(files, childPath)
		}
	}

	return nil
}
コード例 #14
0
ファイル: untag.go プロジェクト: Konubinix/tmsu
func untagPaths(paths, tagArgs []string, recursive bool) error {
	store, err := storage.Open()
	if err != nil {
		return fmt.Errorf("could not open storage: %v", err)
	}
	defer store.Close()
	defer store.Commit()

	tagValuePairs := make([]TagValuePair, 0, 10)
	wereErrors := false
	for _, tagArg := range tagArgs {
		var tagName, valueName string
		index := strings.Index(tagArg, "=")

		switch index {
		case -1, 0:
			tagName = tagArg
		default:
			tagName = tagArg[0:index]
			valueName = tagArg[index+1 : len(tagArg)]
		}

		tag, err := store.TagByName(tagName)
		if err != nil {
			return fmt.Errorf("could not retrieve tag '%v': %v", tagName, err)
		}
		if tag == nil {
			log.Warnf("no such tag '%v'", tagName)
			wereErrors = true
		}

		value, err := store.ValueByName(valueName)
		if err != nil {
			return fmt.Errorf("could not retrieve value '%v': %v", valueName, err)
		}
		if value == nil {
			log.Warnf("no such value '%v'", valueName)
			wereErrors = true
		}

		if tag != nil && value != nil {
			tagValuePairs = append(tagValuePairs, TagValuePair{tag.Id, value.Id})
		}
	}

	for _, path := range paths {
		absPath, err := filepath.Abs(path)
		if err != nil {
			return fmt.Errorf("%v: could not get absolute path: %v", path, err)
		}

		file, err := store.FileByPath(absPath)
		if err != nil {
			return fmt.Errorf("%v: could not retrieve file: %v", path, err)
		}
		if file == nil {
			log.Warnf("%v: file is not tagged", path)
			wereErrors = true
			continue
		}

		log.Infof(2, "%v: unapplying tags.", file.Path())

		for _, tagValuePair := range tagValuePairs {
			if err := store.DeleteFileTag(file.Id, tagValuePair.TagId, tagValuePair.ValueId); err != nil {
				return fmt.Errorf("%v: could not remove tag #%v, value #%v: %v", file.Path(), tagValuePair.TagId, tagValuePair.ValueId, err)
			}
		}

		if recursive {
			childFiles, err := store.FilesByDirectory(file.Path())
			if err != nil {
				return fmt.Errorf("%v: could not retrieve files for directory: %v", file.Path())
			}

			for _, childFile := range childFiles {
				log.Infof(2, "%v: unapplying tags.", childFile.Path())

				for _, tagValuePair := range tagValuePairs {
					if err := store.DeleteFileTag(childFile.Id, tagValuePair.TagId, tagValuePair.ValueId); err != nil {
						return fmt.Errorf("%v: could not remove tag #%v, value #%v: %v", childFile.Path(), tagValuePair.TagId, tagValuePair.ValueId, err)
					}
				}
			}
		}
	}

	if wereErrors {
		return blankError
	}

	return nil
}
コード例 #15
0
ファイル: tag.go プロジェクト: Konubinix/tmsu
func tagPaths(tagArgs, paths []string, recursive bool) error {
	store, err := storage.Open()
	if err != nil {
		return fmt.Errorf("could not open storage: %v", err)
	}
	defer store.Close()
	defer store.Commit()

	fingerprintAlgorithmSetting, err := store.Setting("fingerprintAlgorithm")
	if err != nil {
		return fmt.Errorf("could not retrieve fingerprint algorithm: %v", err)
	}

	tagValuePairs := make([]TagValuePair, 0, 10)
	for _, tagArg := range tagArgs {
		var tagName, valueName string
		index := strings.Index(tagArg, "=")

		switch index {
		case -1, 0:
			tagName = tagArg
		default:
			tagName = tagArg[0:index]
			valueName = tagArg[index+1 : len(tagArg)]
		}

		tag, err := getOrCreateTag(store, tagName)
		if err != nil {
			return err
		}

		value, err := getOrCreateValue(store, valueName)
		if err != nil {
			return err
		}

		tagValuePairs = append(tagValuePairs, TagValuePair{tag.Id, value.Id})
	}

	wereErrors := false
	for _, path := range paths {
		if err := tagPath(store, path, tagValuePairs, recursive, fingerprintAlgorithmSetting.Value); err != nil {
			switch {
			case os.IsPermission(err):
				log.Warnf("%v: permisison denied", path)
				wereErrors = true
			case os.IsNotExist(err):
				log.Warnf("%v: no such file", path)
				wereErrors = true
			default:
				return fmt.Errorf("%v: could not stat file: %v", path, err)
			}
		}
	}

	if wereErrors {
		return blankError
	}

	return nil
}
コード例 #16
0
ファイル: merge.go プロジェクト: Konubinix/tmsu
func mergeExec(options Options, args []string) error {
	if len(args) < 2 {
		return fmt.Errorf("too few arguments.")
	}

	store, err := storage.Open()
	if err != nil {
		return fmt.Errorf("could not open storage: %v", err)
	}
	defer store.Close()
	defer store.Commit()

	destTagName := args[len(args)-1]
	destTag, err := store.TagByName(destTagName)
	if err != nil {
		return fmt.Errorf("could not retrieve tag '%v': %v", destTagName, err)
	}
	if destTag == nil {
		return fmt.Errorf("no such tag '%v'.", destTagName)
	}

	wereErrors := false
	for _, sourceTagName := range args[0 : len(args)-1] {
		if sourceTagName == destTagName {
			log.Warnf("cannot merge tag '%v' into itself.", sourceTagName)
			wereErrors = true
		}

		sourceTag, err := store.TagByName(sourceTagName)
		if err != nil {
			return fmt.Errorf("could not retrieve tag '%v': %v", sourceTagName, err)
		}
		if sourceTag == nil {
			log.Warnf("no such tag '%v'.", sourceTagName)
			wereErrors = true
			continue
		}

		log.Infof(2, "finding files tagged '%v'.", sourceTagName)

		fileTags, err := store.FileTagsByTagId(sourceTag.Id)
		if err != nil {
			return fmt.Errorf("could not retrieve files for tag '%v': %v", sourceTagName, err)
		}

		log.Infof(2, "applying tag '%v' to these files.", destTagName)

		for _, fileTag := range fileTags {
			_, err = store.AddFileTag(fileTag.FileId, destTag.Id, fileTag.ValueId)
			if err != nil {
				return fmt.Errorf("could not apply tag '%v' to file #%v: %v", destTagName, fileTag.FileId, err)
			}
		}

		log.Infof(2, "deleting tag '%v'.", sourceTagName)

		err = store.DeleteTag(sourceTag.Id)
		if err != nil {
			return fmt.Errorf("could not delete tag '%v': %v", sourceTagName, err)
		}
	}

	if wereErrors {
		return blankError
	}

	return nil
}
コード例 #17
0
ファイル: tags.go プロジェクト: Konubinix/tmsu
func listTagsForPaths(store *storage.Storage, paths []string, showCount, onePerLine bool) error {
	wereErrors := false
	for _, path := range paths {
		log.Infof(2, "%v: retrieving tags.", path)

		file, err := store.FileByPath(path)
		if err != nil {
			log.Warn(err.Error())
			continue
		}

		var tagNames []string
		if file != nil {
			fileTags, err := store.FileTagsByFileId(file.Id)
			if err != nil {
				return err
			}

			tagNames, err = lookupTagNames(store, fileTags)
			if err != nil {
				return err
			}
		} else {
			_, err := os.Stat(path)
			if err != nil {
				switch {
				case os.IsPermission(err):
					log.Warnf("%v: permission denied", path)
					wereErrors = true
					continue
				case os.IsNotExist(err):
					log.Warnf("%v: no such file", path)
					wereErrors = true
					continue
				default:
					return fmt.Errorf("%v: could not stat file: %v", path, err)
				}
			}
		}

		if showCount {
			fmt.Println(path + ": " + strconv.Itoa(len(tagNames)))
		} else {
			if onePerLine {
				fmt.Println(path)
				for _, tagName := range tagNames {
					fmt.Println(tagName)
				}
				fmt.Println()
			} else {
				fmt.Println(path + ": " + strings.Join(tagNames, " "))
			}
		}
	}

	if wereErrors {
		return blankError
	}

	return nil
}
コード例 #18
0
ファイル: dupes.go プロジェクト: Konubinix/tmsu
func findDuplicatesOf(paths []string, recursive bool) error {
	store, err := storage.Open()
	if err != nil {
		return fmt.Errorf("could not open storage: %v", err)
	}
	defer store.Close()

	fingerprintAlgorithmSetting, err := store.Setting("fingerprintAlgorithm")
	if err != nil {
		return fmt.Errorf("could not retrieve fingerprint algorithm: %v", err)
	}

	wereErrors := false
	for _, path := range paths {
		_, err := os.Stat(path)
		if err != nil {
			switch {
			case os.IsNotExist(err):
				log.Warnf("%v: no such file", path)
				wereErrors = true
				continue
			case os.IsPermission(err):
				log.Warnf("%v: permission denied", path)
				wereErrors = true
				continue
			default:
				return err
			}
		}
	}

	if wereErrors {
		return blankError
	}

	if recursive {
		p, err := _path.Enumerate(paths)
		if err != nil {
			return fmt.Errorf("could not enumerate paths: %v", err)
		}

		paths = make([]string, len(p))
		for index, path := range p {
			paths[index] = path.Path
		}
	}

	first := true
	for _, path := range paths {
		log.Infof(2, "%v: identifying duplicate files.", path)

		fp, err := fingerprint.Create(path, fingerprintAlgorithmSetting.Value)
		if err != nil {
			return fmt.Errorf("%v: could not create fingerprint: %v", path, err)
		}

		if fp == fingerprint.Fingerprint("") {
			continue
		}

		files, err := store.FilesByFingerprint(fp)
		if err != nil {
			return fmt.Errorf("%v: could not retrieve files matching fingerprint '%v': %v", path, fp, err)
		}

		absPath, err := filepath.Abs(path)
		if err != nil {
			return fmt.Errorf("%v: could not determine absolute path: %v", path, err)
		}

		// filter out the file we're searching on
		dupes := files.Where(func(file *entities.File) bool { return file.Path() != absPath })

		if len(paths) > 1 && len(dupes) > 0 {
			if first {
				first = false
			} else {
				fmt.Println()
			}

			fmt.Printf("%v:\n", path)

			for _, dupe := range dupes {
				relPath := _path.Rel(dupe.Path())
				fmt.Printf("  %v\n", relPath)
			}
		} else {
			for _, dupe := range dupes {
				relPath := _path.Rel(dupe.Path())
				fmt.Println(relPath)
			}
		}
	}

	return nil
}