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 home_folder
= "HOME".environ
72 return replace
("~", home_folder
)
76 # Hack to keep track of the real directory name associated to this pattern
77 redef class BM_Pattern
80 init with_dir
(motif
, dir
: String)
88 var opts_context
= new OptionContext
89 var opt_help
= new OptionBool("Print this help message", "-h", "--help")
90 var opt_list_series
= new OptionBool("Only list the folders in the regex target directories", "--list-series")
91 var opt_verbose
= new OptionBool("Print information about the operations", "-v", "--verbose")
92 var opt_dry_run
= new OptionBool("Simulate work without modifying the filesystem", "--dry-run")
96 opts_context
.add_option
(opt_help
, opt_list_series
, opt_verbose
, opt_dry_run
)
99 fun run
(source_dir
, dest_dir
: String, regex_source_dirs
: Sequence[String], older_than
: TimeT )
101 # manage command line options
103 opts_context
.parse
(args
)
104 if not opts_context
.rest
.is_empty
or opt_help
.value
then
105 print
"Usage: {sys.program_name} [Options]"
109 if opt_help
.value
then exit
0
113 # replace `~` by home path
114 source_dir
= source_dir
.replace_tilde
115 dest_dir
= dest_dir
.replace_tilde
116 for f
in [0..regex_source_dirs
.length
[ do
117 regex_source_dirs
[f
] = regex_source_dirs
[f
].replace_tilde
120 # check config validity
122 failed
= failed
or source_dir
.check_file_existence
123 failed
= failed
or dest_dir
.check_file_existence
124 for dir
in regex_source_dirs
do failed
= failed
or dir
.check_file_existence
125 if failed
then exit
1
127 # collect possible series dir names
128 var dirs_name
= new HashSet[String]
129 for dir
in regex_source_dirs
do for file
in dir
.files
do dirs_name
.add
(file
)
131 # if asked only to print ou the list of series, do so and quit
132 if opt_list_series
.value
then
133 print dirs_name
.join
(", ")
138 var patterns
= new HashSet[BM_Pattern]
139 for dir
in dirs_name
do
140 patterns
.add
new BM_Pattern.with_dir
(dir
, dir
)
141 patterns
.add
new BM_Pattern.with_dir
(dir
.replace
(' ', "."), dir
)
142 patterns
.add
new BM_Pattern.with_dir
(dir
.replace
(' ', "_"), dir
)
145 # compare source files with patterns and sort
146 for file
in source_dir
.files
do
147 var full_source
= source_dir
+ "/" + file
148 var stat
= full_source
.file_lstat
150 # if not a file or dir, skip
151 if not stat
.is_reg
and not stat
.is_dir
then continue
154 var time
= new TimeT.from_i
(stat
.mtime
)
155 if time
.to_i
> older_than
.to_i
then continue
157 # does it fit our regexxes?
158 var move_to_dir
: nullable String = null
159 for pattern
in patterns
do if file
.search
(pattern
) != null then
160 move_to_dir
= pattern
.dir
165 if move_to_dir
!= null then
166 var full_dir_dest
= dest_dir
+ "/" + move_to_dir
167 var full_dest
= full_dir_dest
+ "/" + file
169 if opt_verbose
.value
then print
"moving {full_source} -> {full_dest}"
171 if not opt_dry_run
.value
then
172 if not full_dir_dest
.file_exists
then full_dir_dest
.mkdir
174 var res
= full_source
.file_rename_to
(full_dest
)
176 print
"Moving error: {res}"
185 var config
= new Config
186 var sorter
= new XySorter
188 # calculate cut off time using `elapsed_days` compared to now
190 var wanted_elapsed_secs
= config
.elapsed_days
* 24 * 60 * 60
191 var from_time
= new TimeT.from_i
(now
.to_i
- wanted_elapsed_secs
)
193 sorter
.run
(config
.source_dir
, config
.dest_dir
, config
.regex_source_dirs
, from_time
)