--- /dev/null
+# sort_downloads
+
+This scripts sorts files from a given directory to subfolders in the destination directory. It uses directory names as patterns to sort the files and thus moves each file to a directory with a similar name. Multiple directories may be used to specify the patterns to look for, but the files will only be copied to subfolders in the target directory. This features ensures a clean support for a multi-disk system.
+
+# Installation and usage
+
+If you're lucky enough to have a Nit interpreter, directly run `src/sort_downloads.nit`.
+
+Otherwise, compile with `make` and run with `bin/sort_downloads`.
+
+Check `sort_downloads --help` for command line options.
+
+# Configuration
+
+The main configuration is in the header of `sort_downloads.nit`, in the config class. You may modify it as you need. Notice that the `~` is supported.
+
+You can also have alternative configurations by adding another Nit module, refining the Config class and calling super as main. See the scenario "Sort only older files" for an example.
+
+# Scenarios
+
+Here are some usage scenario.
+
+## Music downloads
+
+You download all of your music to the `~/Downloads` directory but want to sort them in the subfolders of `~/Music`. You must first make sure that there are the appropriate folders in the `~/Music` directory, probably one per artist. Then modify the Config class in `sort_downloads.nit` with something like this:
+
+ class Config
+ var source_dir = "~/Downloads/"
+ var dest_dir = "~/Music/"
+ var regex_source_dirs: Array[String] = ["~/Music/"]
+ var elapsed_days = 0
+ end
+
+## New hard drive
+
+Your old hard drive is full and you bought a new one. You now want to copy your music only on the new hard drive without having to recreate all the folders. You can still use the old folders as a reference for patterns and only copy to the new hard drive. Modify the Config class like so:
+
+ class Config
+ var source_dir = "~/Downloads/"
+ var dest_dir = "/media/new-drive/Music/"
+ var regex_source_dirs: Array[String] = ["~/Music/", dest_dir] # here we use the local variable dest_dir
+ var elapsed_days = 0
+ end
+
+## Multiple configurations, sort videos
+
+If you need more than one configuration, let's say to sort videos, you can use Nit class refinement. Create a separate Nit module next to `src/sort_downloads.nit` named `sort_videos.nit` and use something like:
+
+ #!/usr/bin/env nit
+
+ import sort_downloads
+
+ redef class Config
+ redef fun source_dir do return "/media/new-drive/video-downloads"
+ redef fun dest_dir do return "/media/new-drive/Videos"
+ redef fun regex_source_dirs do return ["~/Videos", dest_dir]
+ redef fun elapsed_days do return 0
+ end
+
+ super # this executes the program
+
+## Sort only older files
+
+You want to automate the process with a cron entry, but still want to have your files available in the download directory for a few days. You can use the `elapsed_days` attribute of the Config class. When set to 7, only files that have not been modified for 7 days will be sorted and moved.
+
+# Author and license
+
+Created by Alexis Laferrière
+
+Licensed under the Apache License Version 2.0
--- /dev/null
+#!/usr/bin/env nit
+
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2013 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Smart file sorting using folder names to sort files. Has built in support for
+# a distributed or multi-disk setup.
+module sort_downloads
+
+# TODO sort patterns by longer first
+# TODO allow for config overriding using `opts`
+
+import opts
+
+`{
+#include <errno.h>
+`}
+
+# Local config
+# Modify these according to your needs. It is also possible to have alternates
+# using class refinment and a main calling to super.
+class Config
+ # Source directory where are the files to be sorted.
+ var source_dir = "~/Downloads/"
+
+ # Destination super directory where classification directories will be
+ # created to hold files.
+ var dest_dir = "~/Videos/"
+
+ # Super directories with wanted folder names, which will be used to sort
+ # the files (only their name are used, the files won't be copied there).
+ var regex_source_dirs: Array[String] = ["~/Videos/"]
+
+ # Will only sort files older than the number of `elapsed_days`.
+ var elapsed_days = 7
+end
+
+redef class String
+ fun check_file_existence: Bool
+ do
+ if not file_exists then
+ print "config error: file \"{self}\" does not exists."
+ return true
+ else return false
+ end
+
+ # Returns null on success
+ fun file_rename_to(dest: String): nullable String import String::to_cstring,
+ String::from_cstring, String as nullable `{
+ int res = rename(String_to_cstring(recv), String_to_cstring(dest));
+ if (res == 0) return null_String();
+ return String_as_nullable(new_String_from_cstring(strerror(errno)));
+ `}
+
+ # Replace `~` by the path to the home diretory
+ fun replace_tilde: String
+ do
+ var home_folder = "HOME".environ
+ return replace("~", home_folder)
+ end
+end
+
+# Hack to keep track of the real directory name associated to this pattern
+redef class BM_Pattern
+ var dir: String
+
+ init with_dir(motif, dir: String)
+ do
+ init(motif)
+ self.dir = dir
+ end
+end
+
+class XySorter
+ var opts_context = new OptionContext
+ var opt_help = new OptionBool("Print this help message", "-h", "--help")
+ var opt_list_series = new OptionBool("Only list the folders in the regex target directories", "--list-series")
+ var opt_verbose = new OptionBool("Print information about the operations", "-v", "--verbose")
+ var opt_dry_run = new OptionBool("Simulate work without modifying the filesystem", "--dry-run")
+
+ fun fill_opts_context
+ do
+ opts_context.add_option(opt_help, opt_list_series, opt_verbose, opt_dry_run)
+ end
+
+ fun run(source_dir, dest_dir: String, regex_source_dirs: Sequence[String], older_than: TimeT )
+ do
+ # manage command line options
+ fill_opts_context
+ opts_context.parse(args)
+ if not opts_context.rest.is_empty or opt_help.value then
+ print "Usage: {sys.program_name} [Options]"
+ print "Options:"
+ opts_context.usage
+
+ if opt_help.value then exit 0
+ exit 1
+ end
+
+ # replace `~` by home path
+ source_dir = source_dir.replace_tilde
+ dest_dir = dest_dir.replace_tilde
+ for f in [0..regex_source_dirs.length[ do
+ regex_source_dirs[f] = regex_source_dirs[f].replace_tilde
+ end
+
+ # check config validity
+ var failed = false
+ failed = failed or source_dir.check_file_existence
+ failed = failed or dest_dir.check_file_existence
+ for dir in regex_source_dirs do failed = failed or dir.check_file_existence
+ if failed then exit 1
+
+ # collect possible series dir names
+ var dirs_name = new HashSet[String]
+ for dir in regex_source_dirs do for file in dir.files do dirs_name.add(file)
+
+ # if asked only to print ou the list of series, do so and quit
+ if opt_list_series.value then
+ print dirs_name.join(", ")
+ return
+ end
+
+ # build regexxes
+ var patterns = new HashSet[BM_Pattern]
+ for dir in dirs_name do
+ patterns.add new BM_Pattern.with_dir(dir, dir)
+ patterns.add new BM_Pattern.with_dir(dir.replace(' ', "."), dir)
+ patterns.add new BM_Pattern.with_dir(dir.replace(' ', "_"), dir)
+ end
+
+ # compare source files with patterns and sort
+ for file in source_dir.files do
+ var full_source = source_dir + "/" + file
+ var stat = full_source.file_lstat
+
+ # if not a file or dir, skip
+ if not stat.is_reg and not stat.is_dir then continue
+
+ # is it old enough?
+ var time = new TimeT.from_i(stat.mtime)
+ if time.to_i > older_than.to_i then continue
+
+ # does it fit our regexxes?
+ var move_to_dir: nullable String = null
+ for pattern in patterns do if file.search(pattern) != null then
+ move_to_dir = pattern.dir
+ break
+ end
+
+ # lets' move it!
+ if move_to_dir != null then
+ var full_dir_dest = dest_dir + "/" + move_to_dir
+ var full_dest = full_dir_dest + "/" + file
+
+ if opt_verbose.value then print "moving {full_source} -> {full_dest}"
+
+ if not opt_dry_run.value then
+ if not full_dir_dest.file_exists then full_dir_dest.mkdir
+
+ var res = full_source.file_rename_to(full_dest)
+ if res != null then
+ print "Moving error: {res}"
+ abort
+ end
+ end
+ end
+ end
+ end
+end
+
+var config = new Config
+var sorter = new XySorter
+
+# calculate cut off time using `elapsed_days` compared to now
+var now = new TimeT
+var wanted_elapsed_secs = config.elapsed_days * 24 * 60 * 60
+var from_time = new TimeT.from_i(now.to_i - wanted_elapsed_secs)
+
+sorter.run(config.source_dir, config.dest_dir, config.regex_source_dirs, from_time)