3 # This file is part of NIT ( http://www.nitlanguage.org ).
5 # Copyright 2013 Alexis Laferrière <alexis.laf@xymus.net>
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
11 # http://www.apache.org/licenses/LICENSE-2.0
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # Smart file sorting using folder names to sort files. Has built in support for
20 # a distributed or multi-disk setup.
23 # TODO sort patterns by longer first
24 # TODO allow for config overriding using `opts`
33 # Modify these according to your needs. It is also possible to have alternates
34 # using class refinment and a main calling to super.
36 # Source directory where are the files to be sorted.
37 var source_dir
= "~/Downloads/"
39 # Destination super directory where classification directories will be
40 # created to hold files.
41 var dest_dir
= "~/Videos/"
43 # Super directories with wanted folder names, which will be used to sort
44 # the files (only their name are used, the files won't be copied there).
45 var regex_source_dirs
: Array[String] = ["~/Videos/"]
47 # Will only sort files older than the number of `elapsed_days`.
52 fun check_file_existence
: Bool
54 if not file_exists
then
55 print
"config error: file \"{self}\
" does not exists."
60 # Returns null on success
61 fun file_rename_to
(dest
: String): nullable String import String::to_cstring
,
62 String::from_cstring
, String as nullable `{
63 int res = rename(String_to_cstring(recv), String_to_cstring(dest));
64 if (res == 0) return null_String();
65 return String_as_nullable(new_String_from_cstring(strerror(errno)));
68 # Replace `~` by the path to the home diretory
69 fun replace_tilde
: String
71 var match
= search
("~/")
72 if match
!= null and match
.from
== 0 then
73 var home_folder
= "HOME".environ
74 return "{home_folder}/{substring(match.after, length)}"
79 # Keeps track of the real directory name associated to this pattern
85 init with_dir
(motif
, dir
: String)
93 var opts_context
= new OptionContext
94 var opt_help
= new OptionBool("Print this help message", "-h", "--help")
95 var opt_list_series
= new OptionBool("Only list the folders in the regex target directories", "--list-series")
96 var opt_verbose
= new OptionBool("Print information about the operations", "-v", "--verbose")
97 var opt_dry_run
= new OptionBool("Simulate work without modifying the filesystem", "--dry-run")
101 opts_context
.add_option
(opt_help
, opt_list_series
, opt_verbose
, opt_dry_run
)
104 fun run
(source_dir
, dest_dir
: String, regex_source_dirs
: Sequence[String], older_than
: TimeT )
106 # manage command line options
108 opts_context
.parse
(args
)
109 if not opts_context
.rest
.is_empty
or opt_help
.value
then
110 print
"Usage: {sys.program_name} [Options]"
114 if opt_help
.value
then exit
0
118 # replace `~` by home path
119 source_dir
= source_dir
.replace_tilde
120 dest_dir
= dest_dir
.replace_tilde
121 for f
in [0..regex_source_dirs
.length
[ do
122 regex_source_dirs
[f
] = regex_source_dirs
[f
].replace_tilde
125 # check config validity
127 failed
= failed
or source_dir
.check_file_existence
128 failed
= failed
or dest_dir
.check_file_existence
129 for dir
in regex_source_dirs
do failed
= failed
or dir
.check_file_existence
130 if failed
then exit
1
132 # collect possible series dir names
133 var dirs_name
= new HashSet[String]
134 for dir
in regex_source_dirs
do for file
in dir
.files
do dirs_name
.add
(file
)
136 # if asked only to print ou the list of series, do so and quit
137 if opt_list_series
.value
then
138 print dirs_name
.join
(", ")
143 var patterns
= new HashSet[PatternWithDir]
144 for dir
in dirs_name
do
145 patterns
.add
new PatternWithDir.with_dir
(dir
, dir
)
146 patterns
.add
new PatternWithDir.with_dir
(dir
.replace
(' ', "."), dir
)
147 patterns
.add
new PatternWithDir.with_dir
(dir
.replace
(' ', "_"), dir
)
150 # compare source files with patterns and sort
151 for file
in source_dir
.files
do
152 var full_source
= source_dir
+ "/" + file
153 var stat
= full_source
.file_lstat
155 # if not a file or dir, skip
156 if not stat
.is_reg
and not stat
.is_dir
then continue
159 var time
= new TimeT.from_i
(stat
.mtime
)
160 if time
.to_i
> older_than
.to_i
then continue
162 # does it fit our regexxes?
163 var move_to_dir
: nullable String = null
164 for pattern
in patterns
do if file
.search
(pattern
) != null then
165 move_to_dir
= pattern
.dir
170 if move_to_dir
!= null then
171 var full_dir_dest
= dest_dir
+ "/" + move_to_dir
172 var full_dest
= full_dir_dest
+ "/" + file
174 if opt_verbose
.value
then print
"moving {full_source} -> {full_dest}"
176 if not opt_dry_run
.value
then
177 if not full_dir_dest
.file_exists
then full_dir_dest
.mkdir
179 var res
= full_source
.file_rename_to
(full_dest
)
181 print
"Moving error: {res}"
190 var config
= new Config
191 var sorter
= new XySorter
193 # calculate cut off time using `elapsed_days` compared to now
195 var wanted_elapsed_secs
= config
.elapsed_days
* 24 * 60 * 60
196 var from_time
= new TimeT.from_i
(now
.to_i
- wanted_elapsed_secs
)
198 sorter
.run
(config
.source_dir
, config
.dest_dir
, config
.regex_source_dirs
, from_time
)