2893 |
2893 |
2894 isAbstract |
2894 isAbstract |
2895 ^ self == AbstractFileBrowser |
2895 ^ self == AbstractFileBrowser |
2896 ! ! |
2896 ! ! |
2897 |
2897 |
|
2898 !AbstractFileBrowser class methodsFor:'queries-file'! |
|
2899 |
|
2900 getBestDirectoryFrom:directories |
|
2901 "from a set of directories, return the common parent directory" |
|
2902 |
|
2903 |stringCol firstPre| |
|
2904 |
|
2905 directories isEmpty ifTrue:[^ nil]. |
|
2906 directories size == 1 ifTrue:[^ directories first]. |
|
2907 stringCol := (directories collect:[:file| file asString]) asOrderedCollection. |
|
2908 firstPre := stringCol at:1. |
|
2909 stringCol from:2 do:[:el| |
|
2910 firstPre := firstPre commonPrefixWith:el. |
|
2911 ]. |
|
2912 (firstPre endsWith:(OperatingSystem fileSeparator)) ifTrue:[ |
|
2913 firstPre removeLast. |
|
2914 ]. |
|
2915 ^ firstPre asFilename |
|
2916 ! ! |
|
2917 |
2898 !AbstractFileBrowser class methodsFor:'resources'! |
2918 !AbstractFileBrowser class methodsFor:'resources'! |
2899 |
2919 |
2900 classResources |
2920 classResources |
2901 ^ FileBrowser classResources |
2921 ^ FileBrowser classResources |
2902 ! ! |
2922 ! ! |
3132 withLimit:limitOrNil |
3152 withLimit:limitOrNil |
3133 lastPart:showLastPartOrNil |
3153 lastPart:showLastPartOrNil |
3134 characterEncoding:characterEncoding |
3154 characterEncoding:characterEncoding |
3135 |
3155 |
3136 "Created: / 12-11-2017 / 12:02:05 / cg" |
3156 "Created: / 12-11-2017 / 12:02:05 / cg" |
|
3157 ! ! |
|
3158 |
|
3159 !AbstractFileBrowser class methodsFor:'utilities - files'! |
|
3160 |
|
3161 allFilesInDirectories:directories forWhich:aBlock |
|
3162 |allFiles| |
|
3163 |
|
3164 directories isEmpty ifTrue:[^ #()]. |
|
3165 |
|
3166 allFiles := OrderedCollection new. |
|
3167 directories do:[:dir| |
|
3168 [ |
|
3169 |fileNames| |
|
3170 |
|
3171 fileNames := dir directoryContents. |
|
3172 fileNames notNil ifTrue:[ |
|
3173 fileNames := fileNames |
|
3174 collect:[:fn | dir construct:fn] |
|
3175 thenSelect:[:fn | fn isDirectory not and:[aBlock value:fn]]. |
|
3176 allFiles addAll:fileNames. |
|
3177 ] |
|
3178 ] on:FileStream openErrorSignal do:[:ex| |
|
3179 self warn:(self classResources stringWithCRs:'Cannot access: %1\(%2)' |
|
3180 with:ex pathName |
|
3181 with:ex description). |
|
3182 ex proceedWith:nil. |
|
3183 ]. |
|
3184 ]. |
|
3185 |
|
3186 ^ allFiles |
|
3187 ! |
|
3188 |
|
3189 fileFindDuplicatesIn:directories |
|
3190 "scan directories for duplicate files. |
|
3191 return a dictionary mapping files to their duplicate(s); |
|
3192 the oldest file found will be the key, the younger files in the value (a collection). |
|
3193 Files without duplicate(s) will not have an entry in the dictionary." |
|
3194 |
|
3195 |infoDir filesBySize anySameSized |
|
3196 result allFiles| |
|
3197 |
|
3198 directories isEmpty ifTrue:[^ #()]. |
|
3199 |
|
3200 allFiles := self allFilesInDirectories:directories forWhich:[:f | true]. |
|
3201 allFiles isEmpty ifTrue:[^ #()]. |
|
3202 |
|
3203 "/ for each, get the file's info (size, modificationTime etc.). |
|
3204 infoDir := Dictionary new. |
|
3205 allFiles do:[:fn | |
|
3206 infoDir at:fn put:(fn info) |
|
3207 ]. |
|
3208 |
|
3209 "/ in a first pass, collect groups by the same size |
|
3210 "/ remember those fileSizes for which multiple files exist |
|
3211 filesBySize := Dictionary new. |
|
3212 anySameSized := false. |
|
3213 infoDir keysAndValuesDo:[:fn :info | |
|
3214 |sz entry| |
|
3215 |
|
3216 sz := info fileSize. |
|
3217 entry := filesBySize at:sz ifAbsentPut:[Set new]. |
|
3218 entry add:fn. |
|
3219 entry size > 1 ifTrue:[ anySameSized := true ]. |
|
3220 ]. |
|
3221 |
|
3222 "/ any of same size ? |
|
3223 anySameSized ifFalse:[^ #() ]. |
|
3224 |
|
3225 result := Dictionary new. |
|
3226 |
|
3227 "/ now walk over the same-sized files and compare the contents. |
|
3228 filesBySize do:[:entry | |
|
3229 |files| |
|
3230 |
|
3231 entry size > 1 ifTrue:[ |
|
3232 files := entry asArray. |
|
3233 files |
|
3234 sort:[:a :b | |
|
3235 |tA tB| |
|
3236 tA := (infoDir at:a) modificationTime. |
|
3237 tB := (infoDir at:b) modificationTime. |
|
3238 tA < tB |
|
3239 ]. |
|
3240 |
|
3241 1 to:files size-1 do:[:idx1 | |
|
3242 |fn1| |
|
3243 |
|
3244 fn1 := files at:idx1. |
|
3245 idx1+1 to:files size do:[:idx2 | |
|
3246 |fn2| |
|
3247 |
|
3248 fn2 := files at:idx2. |
|
3249 |
|
3250 (result at:fn2 ifAbsent:nil) ~= fn1 ifTrue:[ |
|
3251 "/ compare the files |
|
3252 (fn1 sameContentsAs:fn2) ifTrue:[ |
|
3253 "/ Transcript show:'Same: '; show:fn1 baseName; show:' and '; showCR:fn2 baseName. |
|
3254 result at:fn1 put:fn2. |
|
3255 ] |
|
3256 ] |
|
3257 ] |
|
3258 ] |
|
3259 ] |
|
3260 ]. |
|
3261 |
|
3262 result := result associations. |
|
3263 result := result collect:[:assoc | |
|
3264 |f1 f2| |
|
3265 |
|
3266 f1 := assoc key asString. |
|
3267 f2 := assoc value asString. |
|
3268 f1 < f2 ifTrue:[ |
|
3269 f2 -> f1 |
|
3270 ] ifFalse:[ |
|
3271 f1 -> f2 |
|
3272 ] |
|
3273 ]. |
|
3274 "/ result sort:[:f1 :f2 | f1 key > f2 key "f2 value < f1 key value"]. |
|
3275 result sort:[:f1 :f2 | " f1 key > f2 key" f2 value < f1 key value]. |
|
3276 ^ result |
3137 ! ! |
3277 ! ! |
3138 |
3278 |
3139 !AbstractFileBrowser methodsFor:'actions'! |
3279 !AbstractFileBrowser methodsFor:'actions'! |
3140 |
3280 |
3141 askForCommandFor:fileName thenDo:aBlock |
3281 askForCommandFor:fileName thenDo:aBlock |
6431 ! ! |
6571 ! ! |
6432 |
6572 |
6433 !AbstractFileBrowser methodsFor:'menu actions-tools'! |
6573 !AbstractFileBrowser methodsFor:'menu actions-tools'! |
6434 |
6574 |
6435 allFilesInSelectedDirectoriesForWhich:aBlock |
6575 allFilesInSelectedDirectoriesForWhich:aBlock |
6436 |directories allFiles| |
6576 ^ self class allFilesInDirectories:(self currentSelectedDirectories) forWhich:aBlock |
6437 |
|
6438 directories := self currentSelectedDirectories. |
|
6439 directories isEmpty ifTrue:[^ self]. |
|
6440 |
|
6441 allFiles := OrderedCollection new. |
|
6442 directories do:[:dir| |
|
6443 [ |
|
6444 |fileNames| |
|
6445 |
|
6446 fileNames := dir directoryContents. |
|
6447 fileNames notNil ifTrue:[ |
|
6448 fileNames := fileNames |
|
6449 collect:[:fn | dir construct:fn] |
|
6450 thenSelect:[:fn | fn isDirectory not and:[aBlock value:fn]]. |
|
6451 allFiles addAll:fileNames. |
|
6452 ] |
|
6453 ] on:FileStream openErrorSignal do:[:ex| |
|
6454 self warn:(resources stringWithCRs:'Cannot access: %1\(%2)' |
|
6455 with:ex pathName |
|
6456 with:ex description). |
|
6457 ex proceedWith:nil. |
|
6458 ]. |
|
6459 ]. |
|
6460 |
|
6461 ^ allFiles |
|
6462 ! |
6577 ! |
6463 |
6578 |
6464 conversionChainFrom:inSuffix to:outSuffix |
6579 conversionChainFrom:inSuffix to:outSuffix |
6465 |conv| |
6580 |conv| |
6466 |
6581 |
7280 ! |
7395 ! |
7281 |
7396 |
7282 fileFindDuplicates |
7397 fileFindDuplicates |
7283 "scan directory for duplicate files" |
7398 "scan directory for duplicate files" |
7284 |
7399 |
7285 |infoDir filesBySize result info stream textBox maxLength directories allFiles titleStream size| |
7400 |directories duplicatesDictionary info prefixSize stream titleStream textBox maxLength| |
7286 |
7401 |
7287 directories := self currentSelectedDirectories. |
7402 directories := self currentSelectedDirectories. |
7288 |
7403 duplicatesDictionary := self class fileFindDuplicatesIn:directories. |
7289 self withActivityIndicationDo:[ |
7404 duplicatesDictionary isEmpty ifTrue:[ |
7290 result := Dictionary new. |
7405 Dialog information:'No duplicate files found.'. |
7291 |
7406 ^ self. |
7292 directories := self currentSelectedDirectories. |
7407 ]. |
7293 directories isEmpty ifTrue:[^ self]. |
7408 |
7294 |
7409 info := OrderedCollection new. |
7295 allFiles := self allFilesInSelectedDirectoriesForWhich:[:f | true]. |
7410 prefixSize := (self getBestDirectory) asString size. |
7296 |
7411 duplicatesDictionary do:[:assoc | |
7297 infoDir := Dictionary new. |
7412 |fn1 fn2| |
7298 allFiles do:[:fn | |
7413 |
7299 infoDir at:fn put:(fn info) |
7414 fn1 := assoc key. |
7300 ]. |
7415 fn2 := assoc value. |
7301 |
7416 prefixSize > 1 ifTrue:[ |
7302 "/ for each, get the file's size. |
7417 fn1 := ('..', (fn1 copyFrom:(prefixSize + 1))). |
7303 "/ in a first pass, look for files of the same size and |
7418 fn2 := ('..', (fn2 copyFrom:(prefixSize + 1))). |
7304 "/ compare them ... |
7419 ]. |
7305 |
7420 (fn1 includes:Character space) ifTrue:[ |
7306 filesBySize := Dictionary new. |
7421 fn1 := '"' , fn1 , '"' |
7307 infoDir keysAndValuesDo:[:fn :info | |
7422 ]. |
7308 |sz entry| |
7423 (fn2 includes:Character space) ifTrue:[ |
7309 |
7424 fn2 := '"' , fn2 , '"' |
7310 sz := info size. |
7425 ]. |
7311 entry := filesBySize at:sz ifAbsentPut:[Set new]. |
7426 info add:(fn1 , ' same as ' , fn2) |
7312 entry add:fn. |
7427 ]. |
7313 ]. |
7428 |
7314 |
|
7315 "/ any of same size ? |
|
7316 |
|
7317 filesBySize do:[:entry | |
|
7318 |files| |
|
7319 |
|
7320 entry size > 1 ifTrue:[ |
|
7321 files := entry asArray. |
|
7322 1 to:files size-1 do:[:idx1 | |
|
7323 idx1+1 to:files size do:[:idx2 | |
|
7324 |fn1 fn2| |
|
7325 |
|
7326 fn1 := files at:idx1. |
|
7327 fn2 := files at:idx2. |
|
7328 |
|
7329 (result at:fn2 ifAbsent:nil) ~= fn1 ifTrue:[ |
|
7330 "/ compare the files |
|
7331 (fn1 sameContentsAs:fn2) ifTrue:[ |
|
7332 "/ Transcript show:'Same: '; show:fn1 baseName; show:' and '; showCR:fn2 baseName. |
|
7333 result at:fn1 put:fn2. |
|
7334 ] |
|
7335 ] |
|
7336 ] |
|
7337 ] |
|
7338 ] |
|
7339 ]. |
|
7340 |
|
7341 result := result associations. |
|
7342 result := result collect:[:assoc | |
|
7343 |f1 f2| |
|
7344 |
|
7345 f1 := assoc key asString. |
|
7346 f2 := assoc value asString. |
|
7347 f1 < f2 ifTrue:[ |
|
7348 f2 -> f1 |
|
7349 ] ifFalse:[ |
|
7350 f1 -> f2 |
|
7351 ] |
|
7352 ]. |
|
7353 "/ result sort:[:f1 :f2 | f1 key > f2 key "f2 value < f1 key value"]. |
|
7354 result sort:[:f1 :f2 | " f1 key > f2 key" f2 value < f1 key value]. |
|
7355 |
|
7356 info := OrderedCollection new. |
|
7357 size := self getBestDirectory asString size. |
|
7358 result do:[:assoc | |
|
7359 |fn1 fn2| |
|
7360 |
|
7361 fn1 := assoc key. |
|
7362 fn2 := assoc value. |
|
7363 size > 1 ifTrue:[ |
|
7364 fn1 := ('..', (fn1 copyFrom:(size + 1))). |
|
7365 fn2 := ('..', (fn2 copyFrom:(size + 1))). |
|
7366 ]. |
|
7367 (fn1 includes:Character space) ifTrue:[ |
|
7368 fn1 := '"' , fn1 , '"' |
|
7369 ]. |
|
7370 (fn2 includes:Character space) ifTrue:[ |
|
7371 fn2 := '"' , fn2 , '"' |
|
7372 ]. |
|
7373 info add:(fn1 , ' same as ' , fn2) |
|
7374 ]. |
|
7375 info isEmpty ifTrue:[ |
|
7376 Dialog information:'No duplicate files found.'. |
|
7377 ^ self. |
|
7378 ]. |
|
7379 ]. |
|
7380 stream := CharacterWriteStream new. |
7429 stream := CharacterWriteStream new. |
7381 stream nextPutAllLines:info. |
7430 stream nextPutAllLines:info. |
7382 |
7431 |
7383 titleStream := CharacterWriteStream with:'File duplicates in directory'. |
7432 titleStream := CharacterWriteStream with:'File duplicates in directory'. |
7384 directories size == 1 ifTrue:[ |
7433 directories size == 1 ifTrue:[ |
7385 titleStream nextPutAll:'y: '; nextPutAll: directories first asString. |
7434 titleStream nextPutAll:'y: '; nextPutAll: directories first asString. |
7386 ] ifFalse:[ |
7435 ] ifFalse:[ |
7387 titleStream nextPutLine:'ies: '. |
7436 titleStream nextPutLine:'ies: '. |
7388 directories do:[:dir| |
7437 directories do:[:dir| |
7389 size > 1 ifTrue:[ |
7438 prefixSize > 1 ifTrue:[ |
7390 titleStream nextPutAll:'..'. |
7439 titleStream nextPutAll:'..'. |
7391 titleStream nextPutLine:((dir asString) copyFrom:(size + 1)). |
7440 titleStream nextPutLine:((dir asString) copyFrom:(prefixSize + 1)). |
7392 ] ifFalse:[ |
7441 ] ifFalse:[ |
7393 titleStream nextPutLine:(dir asString). |
7442 titleStream nextPutLine:(dir asString). |
7394 ]. |
7443 ]. |
7395 ] |
7444 ] |
7396 ]. |
7445 ]. |