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`
34 # Modify these according to your needs. It is also possible to have alternates
35 # using class refinment and a main calling to super.
37 # Is it configured? Change to "true" when the configuration has been
38 # adapted to your needs and setup
39 var is_configured
= false
41 # Source directory where are the files to be sorted.
42 var source_dir
= "~/Downloads/"
44 # Destination super directory where classification directories will be
45 # created to hold files.
46 var dest_dir
= "~/Videos/"
48 # Super directories with wanted folder names, which will be used to sort
49 # the files (only their name are used, the files won't be copied there).
50 var regex_source_dirs
: Array[String] = ["~/Videos/"]
52 # Will only sort files older than the number of `elapsed_days`.
57 fun check_file_existence
: Bool
59 if not file_exists
then
60 print
"config error: file \"{self}\
" does not exists."
65 # Returns null on success
66 fun file_rename_to
(dest
: String): nullable String import String.to_cstring
,
67 NativeString.to_s
, String.as nullable `{
68 int res = rename(String_to_cstring(self), String_to_cstring(dest));
69 if (res == 0) return null_String();
70 return String_as_nullable(NativeString_to_s(strerror(errno)));
73 # Replace `~` by the path to the home diretory
74 fun replace_tilde
: String
76 var match
= search
("~/")
77 if match
!= null and match
.from
== 0 then
78 var home_folder
= "HOME".environ
79 return "{home_folder}/{substring(match.after, length)}"
84 # Keeps track of the real directory name associated to this pattern
88 var dir
: String is noinit
90 init with_dir
(motif
, dir
: String)
98 var opts_context
= new OptionContext
99 var opt_help
= new OptionBool("Print this help message", "-h", "--help")
100 var opt_list_series
= new OptionBool("Only list the folders in the regex target directories", "--list-series")
101 var opt_verbose
= new OptionBool("Print information about the operations", "-v", "--verbose")
102 var opt_dry_run
= new OptionBool("Simulate work without modifying the filesystem", "--dry-run")
104 fun fill_opts_context
106 opts_context
.add_option
(opt_help
, opt_list_series
, opt_verbose
, opt_dry_run
)
109 fun run
(source_dir
, dest_dir
: String, regex_source_dirs
: Sequence[String], older_than
: TimeT )
111 # manage command line options
113 opts_context
.parse
(args
)
114 if not opts_context
.rest
.is_empty
or opt_help
.value
then
115 print
"Usage: {sys.program_name} [Options]"
119 if opt_help
.value
then exit
0
123 # replace `~` by home path
124 source_dir
= source_dir
.replace_tilde
125 dest_dir
= dest_dir
.replace_tilde
126 for f
in [0..regex_source_dirs
.length
[ do
127 regex_source_dirs
[f
] = regex_source_dirs
[f
].replace_tilde
130 # check config validity
132 failed
= failed
or source_dir
.check_file_existence
133 failed
= failed
or dest_dir
.check_file_existence
134 for dir
in regex_source_dirs
do failed
= failed
or dir
.check_file_existence
135 if failed
then exit
1
137 # collect possible series dir names
138 var dirs_name
= new HashSet[String]
139 for dir
in regex_source_dirs
do for file
in dir
.files
do dirs_name
.add
(file
)
141 # if asked only to print ou the list of series, do so and quit
142 if opt_list_series
.value
then
143 print dirs_name
.join
(", ")
148 var patterns
= new HashSet[PatternWithDir]
149 for dir
in dirs_name
do
150 patterns
.add
new PatternWithDir.with_dir
(dir
, dir
)
151 patterns
.add
new PatternWithDir.with_dir
(dir
.replace
(' ', "."), dir
)
152 patterns
.add
new PatternWithDir.with_dir
(dir
.replace
(' ', "_"), dir
)
155 # compare source files with patterns and sort
156 for file
in source_dir
.files
do
157 var full_source
= source_dir
+ "/" + file
158 var stat
= full_source
.file_lstat
160 # if not a file or dir, skip
161 if not stat
.is_reg
and not stat
.is_dir
then continue
164 var time
= new TimeT.from_i
(stat
.mtime
)
165 if time
.to_i
> older_than
.to_i
then continue
167 # does it fit our regexxes?
168 var move_to_dir
: nullable String = null
169 for pattern
in patterns
do if file
.search
(pattern
) != null then
170 move_to_dir
= pattern
.dir
175 if move_to_dir
!= null then
176 var full_dir_dest
= dest_dir
+ "/" + move_to_dir
177 var full_dest
= full_dir_dest
+ "/" + file
179 if opt_verbose
.value
then print
"moving {full_source} -> {full_dest}"
181 if not opt_dry_run
.value
then
182 if not full_dir_dest
.file_exists
then full_dir_dest
.mkdir
184 var res
= full_source
.file_rename_to
(full_dest
)
186 print
"Moving error: {res}"
195 var config
= new Config
196 if not config
.is_configured
then
197 print
"Not configured, make sure you modify the script and set is_configured to true"
200 var sorter
= new XySorter
202 # calculate cut off time using `elapsed_days` compared to now
204 var wanted_elapsed_secs
= config
.elapsed_days
* 24 * 60 * 60
205 var from_time
= new TimeT.from_i
(now
.to_i
- wanted_elapsed_secs
)
207 sorter
.run
(config
.source_dir
, config
.dest_dir
, config
.regex_source_dirs
, from_time
)