9424ee6742b0c589d64d1c5f7be75ceeeda1ccd8
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 # Is it configured? Change to "true" when the configuration has been
37 # adapted to your needs and setup
38 var is_configured
= false
40 # Source directory where are the files to be sorted.
41 var source_dir
= "~/Downloads/"
43 # Destination super directory where classification directories will be
44 # created to hold files.
45 var dest_dir
= "~/Videos/"
47 # Super directories with wanted folder names, which will be used to sort
48 # the files (only their name are used, the files won't be copied there).
49 var regex_source_dirs
: Array[String] = ["~/Videos/"]
51 # Will only sort files older than the number of `elapsed_days`.
56 fun check_file_existence
: Bool
58 if not file_exists
then
59 print
"config error: file \"{self}\
" does not exists."
64 # Returns null on success
65 fun file_rename_to
(dest
: String): nullable String import String::to_cstring
,
66 String::from_cstring
, String as nullable `{
67 int res = rename(String_to_cstring(recv), String_to_cstring(dest));
68 if (res == 0) return null_String();
69 return String_as_nullable(new_String_from_cstring(strerror(errno)));
72 # Replace `~` by the path to the home diretory
73 fun replace_tilde
: String
75 var match
= search
("~/")
76 if match
!= null and match
.from
== 0 then
77 var home_folder
= "HOME".environ
78 return "{home_folder}/{substring(match.after, length)}"
83 # Keeps track of the real directory name associated to this pattern
89 init with_dir
(motif
, dir
: String)
97 var opts_context
= new OptionContext
98 var opt_help
= new OptionBool("Print this help message", "-h", "--help")
99 var opt_list_series
= new OptionBool("Only list the folders in the regex target directories", "--list-series")
100 var opt_verbose
= new OptionBool("Print information about the operations", "-v", "--verbose")
101 var opt_dry_run
= new OptionBool("Simulate work without modifying the filesystem", "--dry-run")
103 fun fill_opts_context
105 opts_context
.add_option
(opt_help
, opt_list_series
, opt_verbose
, opt_dry_run
)
108 fun run
(source_dir
, dest_dir
: String, regex_source_dirs
: Sequence[String], older_than
: TimeT )
110 # manage command line options
112 opts_context
.parse
(args
)
113 if not opts_context
.rest
.is_empty
or opt_help
.value
then
114 print
"Usage: {sys.program_name} [Options]"
118 if opt_help
.value
then exit
0
122 # replace `~` by home path
123 source_dir
= source_dir
.replace_tilde
124 dest_dir
= dest_dir
.replace_tilde
125 for f
in [0..regex_source_dirs
.length
[ do
126 regex_source_dirs
[f
] = regex_source_dirs
[f
].replace_tilde
129 # check config validity
131 failed
= failed
or source_dir
.check_file_existence
132 failed
= failed
or dest_dir
.check_file_existence
133 for dir
in regex_source_dirs
do failed
= failed
or dir
.check_file_existence
134 if failed
then exit
1
136 # collect possible series dir names
137 var dirs_name
= new HashSet[String]
138 for dir
in regex_source_dirs
do for file
in dir
.files
do dirs_name
.add
(file
)
140 # if asked only to print ou the list of series, do so and quit
141 if opt_list_series
.value
then
142 print dirs_name
.join
(", ")
147 var patterns
= new HashSet[PatternWithDir]
148 for dir
in dirs_name
do
149 patterns
.add
new PatternWithDir.with_dir
(dir
, dir
)
150 patterns
.add
new PatternWithDir.with_dir
(dir
.replace
(' ', "."), dir
)
151 patterns
.add
new PatternWithDir.with_dir
(dir
.replace
(' ', "_"), dir
)
154 # compare source files with patterns and sort
155 for file
in source_dir
.files
do
156 var full_source
= source_dir
+ "/" + file
157 var stat
= full_source
.file_lstat
159 # if not a file or dir, skip
160 if not stat
.is_reg
and not stat
.is_dir
then continue
163 var time
= new TimeT.from_i
(stat
.mtime
)
164 if time
.to_i
> older_than
.to_i
then continue
166 # does it fit our regexxes?
167 var move_to_dir
: nullable String = null
168 for pattern
in patterns
do if file
.search
(pattern
) != null then
169 move_to_dir
= pattern
.dir
174 if move_to_dir
!= null then
175 var full_dir_dest
= dest_dir
+ "/" + move_to_dir
176 var full_dest
= full_dir_dest
+ "/" + file
178 if opt_verbose
.value
then print
"moving {full_source} -> {full_dest}"
180 if not opt_dry_run
.value
then
181 if not full_dir_dest
.file_exists
then full_dir_dest
.mkdir
183 var res
= full_source
.file_rename_to
(full_dest
)
185 print
"Moving error: {res}"
194 var config
= new Config
195 if not config
.is_configured
then
196 print
"Not configured, make sure you modify the script and set is_configured to true"
199 var sorter
= new XySorter
201 # calculate cut off time using `elapsed_days` compared to now
203 var wanted_elapsed_secs
= config
.elapsed_days
* 24 * 60 * 60
204 var from_time
= new TimeT.from_i
(now
.to_i
- wanted_elapsed_secs
)
206 sorter
.run
(config
.source_dir
, config
.dest_dir
, config
.regex_source_dirs
, from_time
)