--- a/AbstractFileBrowser.st Mon Jan 01 10:58:48 2018 +0100
+++ b/AbstractFileBrowser.st Mon Jan 01 11:00:34 2018 +0100
@@ -2895,6 +2895,26 @@
^ self == AbstractFileBrowser
! !
+!AbstractFileBrowser class methodsFor:'queries-file'!
+
+getBestDirectoryFrom:directories
+ "from a set of directories, return the common parent directory"
+
+ |stringCol firstPre|
+
+ directories isEmpty ifTrue:[^ nil].
+ directories size == 1 ifTrue:[^ directories first].
+ stringCol := (directories collect:[:file| file asString]) asOrderedCollection.
+ firstPre := stringCol at:1.
+ stringCol from:2 do:[:el|
+ firstPre := firstPre commonPrefixWith:el.
+ ].
+ (firstPre endsWith:(OperatingSystem fileSeparator)) ifTrue:[
+ firstPre removeLast.
+ ].
+ ^ firstPre asFilename
+! !
+
!AbstractFileBrowser class methodsFor:'resources'!
classResources
@@ -3136,6 +3156,126 @@
"Created: / 12-11-2017 / 12:02:05 / cg"
! !
+!AbstractFileBrowser class methodsFor:'utilities - files'!
+
+allFilesInDirectories:directories forWhich:aBlock
+ |allFiles|
+
+ directories isEmpty ifTrue:[^ #()].
+
+ allFiles := OrderedCollection new.
+ directories do:[:dir|
+ [
+ |fileNames|
+
+ fileNames := dir directoryContents.
+ fileNames notNil ifTrue:[
+ fileNames := fileNames
+ collect:[:fn | dir construct:fn]
+ thenSelect:[:fn | fn isDirectory not and:[aBlock value:fn]].
+ allFiles addAll:fileNames.
+ ]
+ ] on:FileStream openErrorSignal do:[:ex|
+ self warn:(self classResources stringWithCRs:'Cannot access: %1\(%2)'
+ with:ex pathName
+ with:ex description).
+ ex proceedWith:nil.
+ ].
+ ].
+
+ ^ allFiles
+!
+
+fileFindDuplicatesIn:directories
+ "scan directories for duplicate files.
+ return a dictionary mapping files to their duplicate(s);
+ the oldest file found will be the key, the younger files in the value (a collection).
+ Files without duplicate(s) will not have an entry in the dictionary."
+
+ |infoDir filesBySize anySameSized
+ result allFiles|
+
+ directories isEmpty ifTrue:[^ #()].
+
+ allFiles := self allFilesInDirectories:directories forWhich:[:f | true].
+ allFiles isEmpty ifTrue:[^ #()].
+
+ "/ for each, get the file's info (size, modificationTime etc.).
+ infoDir := Dictionary new.
+ allFiles do:[:fn |
+ infoDir at:fn put:(fn info)
+ ].
+
+ "/ in a first pass, collect groups by the same size
+ "/ remember those fileSizes for which multiple files exist
+ filesBySize := Dictionary new.
+ anySameSized := false.
+ infoDir keysAndValuesDo:[:fn :info |
+ |sz entry|
+
+ sz := info fileSize.
+ entry := filesBySize at:sz ifAbsentPut:[Set new].
+ entry add:fn.
+ entry size > 1 ifTrue:[ anySameSized := true ].
+ ].
+
+ "/ any of same size ?
+ anySameSized ifFalse:[^ #() ].
+
+ result := Dictionary new.
+
+ "/ now walk over the same-sized files and compare the contents.
+ filesBySize do:[:entry |
+ |files|
+
+ entry size > 1 ifTrue:[
+ files := entry asArray.
+ files
+ sort:[:a :b |
+ |tA tB|
+ tA := (infoDir at:a) modificationTime.
+ tB := (infoDir at:b) modificationTime.
+ tA < tB
+ ].
+
+ 1 to:files size-1 do:[:idx1 |
+ |fn1|
+
+ fn1 := files at:idx1.
+ idx1+1 to:files size do:[:idx2 |
+ |fn2|
+
+ fn2 := files at:idx2.
+
+ (result at:fn2 ifAbsent:nil) ~= fn1 ifTrue:[
+ "/ compare the files
+ (fn1 sameContentsAs:fn2) ifTrue:[
+ "/ Transcript show:'Same: '; show:fn1 baseName; show:' and '; showCR:fn2 baseName.
+ result at:fn1 put:fn2.
+ ]
+ ]
+ ]
+ ]
+ ]
+ ].
+
+ result := result associations.
+ result := result collect:[:assoc |
+ |f1 f2|
+
+ f1 := assoc key asString.
+ f2 := assoc value asString.
+ f1 < f2 ifTrue:[
+ f2 -> f1
+ ] ifFalse:[
+ f1 -> f2
+ ]
+ ].
+ "/ result sort:[:f1 :f2 | f1 key > f2 key "f2 value < f1 key value"].
+ result sort:[:f1 :f2 | " f1 key > f2 key" f2 value < f1 key value].
+ ^ result
+! !
+
!AbstractFileBrowser methodsFor:'actions'!
askForCommandFor:fileName thenDo:aBlock
@@ -6433,32 +6573,7 @@
!AbstractFileBrowser methodsFor:'menu actions-tools'!
allFilesInSelectedDirectoriesForWhich:aBlock
- |directories allFiles|
-
- directories := self currentSelectedDirectories.
- directories isEmpty ifTrue:[^ self].
-
- allFiles := OrderedCollection new.
- directories do:[:dir|
- [
- |fileNames|
-
- fileNames := dir directoryContents.
- fileNames notNil ifTrue:[
- fileNames := fileNames
- collect:[:fn | dir construct:fn]
- thenSelect:[:fn | fn isDirectory not and:[aBlock value:fn]].
- allFiles addAll:fileNames.
- ]
- ] on:FileStream openErrorSignal do:[:ex|
- self warn:(resources stringWithCRs:'Cannot access: %1\(%2)'
- with:ex pathName
- with:ex description).
- ex proceedWith:nil.
- ].
- ].
-
- ^ allFiles
+ ^ self class allFilesInDirectories:(self currentSelectedDirectories) forWhich:aBlock
!
conversionChainFrom:inSuffix to:outSuffix
@@ -7282,101 +7397,35 @@
fileFindDuplicates
"scan directory for duplicate files"
- |infoDir filesBySize result info stream textBox maxLength directories allFiles titleStream size|
+ |directories duplicatesDictionary info prefixSize stream titleStream textBox maxLength|
directories := self currentSelectedDirectories.
-
- self withActivityIndicationDo:[
- result := Dictionary new.
-
- directories := self currentSelectedDirectories.
- directories isEmpty ifTrue:[^ self].
-
- allFiles := self allFilesInSelectedDirectoriesForWhich:[:f | true].
-
- infoDir := Dictionary new.
- allFiles do:[:fn |
- infoDir at:fn put:(fn info)
- ].
-
- "/ for each, get the file's size.
- "/ in a first pass, look for files of the same size and
- "/ compare them ...
-
- filesBySize := Dictionary new.
- infoDir keysAndValuesDo:[:fn :info |
- |sz entry|
-
- sz := info size.
- entry := filesBySize at:sz ifAbsentPut:[Set new].
- entry add:fn.
- ].
-
- "/ any of same size ?
-
- filesBySize do:[:entry |
- |files|
-
- entry size > 1 ifTrue:[
- files := entry asArray.
- 1 to:files size-1 do:[:idx1 |
- idx1+1 to:files size do:[:idx2 |
- |fn1 fn2|
-
- fn1 := files at:idx1.
- fn2 := files at:idx2.
-
- (result at:fn2 ifAbsent:nil) ~= fn1 ifTrue:[
- "/ compare the files
- (fn1 sameContentsAs:fn2) ifTrue:[
-"/ Transcript show:'Same: '; show:fn1 baseName; show:' and '; showCR:fn2 baseName.
- result at:fn1 put:fn2.
- ]
- ]
- ]
- ]
- ]
- ].
-
- result := result associations.
- result := result collect:[:assoc |
- |f1 f2|
-
- f1 := assoc key asString.
- f2 := assoc value asString.
- f1 < f2 ifTrue:[
- f2 -> f1
- ] ifFalse:[
- f1 -> f2
- ]
- ].
- "/ result sort:[:f1 :f2 | f1 key > f2 key "f2 value < f1 key value"].
- result sort:[:f1 :f2 | " f1 key > f2 key" f2 value < f1 key value].
-
- info := OrderedCollection new.
- size := self getBestDirectory asString size.
- result do:[:assoc |
- |fn1 fn2|
-
- fn1 := assoc key.
- fn2 := assoc value.
- size > 1 ifTrue:[
- fn1 := ('..', (fn1 copyFrom:(size + 1))).
- fn2 := ('..', (fn2 copyFrom:(size + 1))).
- ].
- (fn1 includes:Character space) ifTrue:[
- fn1 := '"' , fn1 , '"'
- ].
- (fn2 includes:Character space) ifTrue:[
- fn2 := '"' , fn2 , '"'
- ].
- info add:(fn1 , ' same as ' , fn2)
- ].
- info isEmpty ifTrue:[
- Dialog information:'No duplicate files found.'.
- ^ self.
- ].
- ].
+ duplicatesDictionary := self class fileFindDuplicatesIn:directories.
+ duplicatesDictionary isEmpty ifTrue:[
+ Dialog information:'No duplicate files found.'.
+ ^ self.
+ ].
+
+ info := OrderedCollection new.
+ prefixSize := (self getBestDirectory) asString size.
+ duplicatesDictionary do:[:assoc |
+ |fn1 fn2|
+
+ fn1 := assoc key.
+ fn2 := assoc value.
+ prefixSize > 1 ifTrue:[
+ fn1 := ('..', (fn1 copyFrom:(prefixSize + 1))).
+ fn2 := ('..', (fn2 copyFrom:(prefixSize + 1))).
+ ].
+ (fn1 includes:Character space) ifTrue:[
+ fn1 := '"' , fn1 , '"'
+ ].
+ (fn2 includes:Character space) ifTrue:[
+ fn2 := '"' , fn2 , '"'
+ ].
+ info add:(fn1 , ' same as ' , fn2)
+ ].
+
stream := CharacterWriteStream new.
stream nextPutAllLines:info.
@@ -7386,9 +7435,9 @@
] ifFalse:[
titleStream nextPutLine:'ies: '.
directories do:[:dir|
- size > 1 ifTrue:[
+ prefixSize > 1 ifTrue:[
titleStream nextPutAll:'..'.
- titleStream nextPutLine:((dir asString) copyFrom:(size + 1)).
+ titleStream nextPutLine:((dir asString) copyFrom:(prefixSize + 1)).
] ifFalse:[
titleStream nextPutLine:(dir asString).
].
@@ -9342,21 +9391,9 @@
!
getBestDirectory
-
- |selFiles stringCol firstPre|
-
- selFiles := self currentSelectedDirectories.
- selFiles isEmpty ifTrue:[^ nil].
- selFiles size == 1 ifTrue:[^ selFiles first].
- stringCol := (selFiles collect:[:file| file asString]) asOrderedCollection.
- firstPre := stringCol at:1.
- stringCol from:2 do:[:el|
- firstPre := firstPre commonPrefixWith:el.
- ].
- (firstPre endsWith:(OperatingSystem fileSeparator)) ifTrue:[
- firstPre removeLast.
- ].
- ^ firstPre asFilename
+ "from a set of directories, return the common parent directory"
+
+ ^ self class getBestDirectoryFrom:(self currentSelectedDirectories)
!
getDirWithoutFileName:aFileName