melon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +8 -0
- data/Gemfile +3 -0
- data/History.txt +6 -0
- data/README.textile +29 -0
- data/Rakefile +18 -0
- data/TODO +15 -0
- data/bin/melon +8 -0
- data/features/basic.feature +33 -0
- data/features/edges.feature +13 -0
- data/features/step_definitions/basic_steps.rb +15 -0
- data/features/support/env.rb +7 -0
- data/lib/melon.rb +1 -0
- data/lib/melon/cli.rb +121 -0
- data/lib/melon/commands.rb +122 -0
- data/lib/melon/commands/add.rb +10 -0
- data/lib/melon/commands/basic_command.rb +19 -0
- data/lib/melon/commands/help.rb +51 -0
- data/lib/melon/example.rb +68 -0
- data/lib/melon/hasher.rb +13 -0
- data/lib/melon/version.rb +10 -0
- data/melon.gemspec +32 -0
- data/script/console +12 -0
- data/spec/melon/cli_spec.rb +8 -0
- data/spec/melon/database_spec.rb +32 -0
- data/spec/melon/fingerprint_spec.rb +30 -0
- data/spec/melon/parsed_arguments_spec.rb +77 -0
- data/spec/spec_helper.rb +61 -0
- metadata +144 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/History.txt
ADDED
data/README.textile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
h1. Melon
|
2
|
+
|
3
|
+
A media file cataloging tool.
|
4
|
+
|
5
|
+
h2. Synopsis
|
6
|
+
|
7
|
+
Melon is a tool for tracking files based on content, aiming
|
8
|
+
to scale to arbitrarily large files without slowing down. It uses
|
9
|
+
a sparse hashing algorithm to generate a digest without having to
|
10
|
+
read the whole file, allowing it to go much faster than disk would
|
11
|
+
ordinarily allow.
|
12
|
+
|
13
|
+
h2. Installation
|
14
|
+
|
15
|
+
<pre>
|
16
|
+
gem install melon
|
17
|
+
</pre>
|
18
|
+
|
19
|
+
h2. Usage
|
20
|
+
|
21
|
+
TODO
|
22
|
+
|
23
|
+
h2. Motivation
|
24
|
+
|
25
|
+
"Is this file copied onto my media drive yet?"
|
26
|
+
|
27
|
+
h2. Author
|
28
|
+
|
29
|
+
Copyright 2010 Andrew Roberts
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'cucumber'
|
3
|
+
require 'cucumber/rake/task'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
|
6
|
+
desc 'Default: run the cucumber features.'
|
7
|
+
task :default => :features
|
8
|
+
|
9
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
10
|
+
t.cucumber_opts = "features --format pretty"
|
11
|
+
end
|
12
|
+
|
13
|
+
eval("$specification = begin; #{IO.read('melon.gemspec')}; end")
|
14
|
+
Rake::GemPackageTask.new($specification) do |package|
|
15
|
+
package.need_zip = true
|
16
|
+
package.need_tar = true
|
17
|
+
end
|
18
|
+
|
data/TODO
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Command line parsing:
|
2
|
+
* melon [<opts>] <cmd> <cmd-params> [<cmd-opts>]
|
3
|
+
* pivot on <cmd>, melon opts come before cmd, cmd-opts come after
|
4
|
+
* this may make it hard to guess at unrecognized commands
|
5
|
+
* commands don't have a dash
|
6
|
+
|
7
|
+
Testing:
|
8
|
+
* fix rcov rake world
|
9
|
+
|
10
|
+
Commands:
|
11
|
+
* a command helper that
|
12
|
+
* dynamically makes the collection "commands" from files in commands/*
|
13
|
+
* figures out the padding for the banner
|
14
|
+
|
15
|
+
* google "ruby get all classes declared in a directory"
|
data/bin/melon
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
Feature: Basic usage
|
2
|
+
|
3
|
+
Background:
|
4
|
+
Given a file named "test_file" with:
|
5
|
+
"""
|
6
|
+
This file is a test file
|
7
|
+
"""
|
8
|
+
|
9
|
+
Scenario: Adding files to a melon database
|
10
|
+
When I run "melon -d test.db add test_file"
|
11
|
+
Then the output should contain a hash
|
12
|
+
And the output should contain "test_file"
|
13
|
+
|
14
|
+
Scenario: Adding a file that already exists
|
15
|
+
When I run "melon -d test.db add test_file"
|
16
|
+
And I run "melon -d test.db add test_file"
|
17
|
+
Then the output should contain:
|
18
|
+
"""
|
19
|
+
melon: path already present in database
|
20
|
+
"""
|
21
|
+
And the exit status should not be 0
|
22
|
+
|
23
|
+
Scenario: Checking a file that is not in the database
|
24
|
+
When I run "melon -d test.db check test_file"
|
25
|
+
Then the output should contain "test_file"
|
26
|
+
And the output should start with "/"
|
27
|
+
|
28
|
+
Scenario: Adding a directory
|
29
|
+
Given a directory named "testo"
|
30
|
+
When I run "melon -d test.db add testo"
|
31
|
+
Then the output should contain "directory"
|
32
|
+
And the exit status should not be 0
|
33
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Feature: Edge cases
|
2
|
+
|
3
|
+
Scenario: Adding two files with the identical contents
|
4
|
+
Given a file named "test_file" with:
|
5
|
+
"""
|
6
|
+
This file is a test file
|
7
|
+
"""
|
8
|
+
When I run "cp test_file test_file_2"
|
9
|
+
And I run "melon -d test.db add test_file test_file_2"
|
10
|
+
Then the stderr should contain:
|
11
|
+
"""
|
12
|
+
melon: file exists elsewhere in the database
|
13
|
+
"""
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'aruba/api'
|
2
|
+
|
3
|
+
Then /^the output should be empty$/ do
|
4
|
+
all_output.should match(/^$/)
|
5
|
+
end
|
6
|
+
|
7
|
+
Then /^the output should start with "([^"]*)"$/ do |arg1|
|
8
|
+
all_output.should match(/^#{arg1}/)
|
9
|
+
end
|
10
|
+
|
11
|
+
Then /^the output should contain a hash$/ do
|
12
|
+
all_output.should match(/[a-fA-F0-9]{20}/)
|
13
|
+
end
|
14
|
+
|
15
|
+
|
data/lib/melon.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'melon/cli'
|
data/lib/melon/cli.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
require 'melon/version'
|
5
|
+
require 'melon/commands'
|
6
|
+
|
7
|
+
module Melon
|
8
|
+
class CLI
|
9
|
+
|
10
|
+
def self.execute(arguments=[])
|
11
|
+
new(arguments).run
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :arguments
|
15
|
+
|
16
|
+
def initialize(arguments)
|
17
|
+
self.arguments = arguments
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.default_options
|
21
|
+
options = OpenStruct.new
|
22
|
+
options.database_path = "#{ENV['HOME']}/.melondb"
|
23
|
+
options
|
24
|
+
end
|
25
|
+
|
26
|
+
def run
|
27
|
+
options = parse_options
|
28
|
+
options.database = PStore.new(File.expand_path(options.database_path))
|
29
|
+
|
30
|
+
# prepare db
|
31
|
+
options.database.transaction do
|
32
|
+
options.database[:by_hash] ||= {}
|
33
|
+
options.database[:by_path] ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
unless arguments.empty?
|
37
|
+
run_command(options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def run_command(options)
|
42
|
+
# look for command class in args.shift
|
43
|
+
command_name = arguments.shift
|
44
|
+
begin
|
45
|
+
command = Commands.const_get(command_name.capitalize)
|
46
|
+
command.new(arguments, options).run
|
47
|
+
# rescue NameError
|
48
|
+
# CLI.error "unrecognized command: #{command_name}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse_options
|
53
|
+
options = CLI.default_options
|
54
|
+
|
55
|
+
parser = OptionParser.new do |p|
|
56
|
+
p.banner = "Usage: melon [options] COMMAND [command-options] [ARGS]"
|
57
|
+
|
58
|
+
p.separator ""
|
59
|
+
p.separator "Commands:"
|
60
|
+
p.separator ""
|
61
|
+
|
62
|
+
%w(add check).each do |command|
|
63
|
+
cls = Commands.const_get(command.capitalize)
|
64
|
+
p.separator format_command(command, cls.description)
|
65
|
+
end
|
66
|
+
|
67
|
+
p.separator ""
|
68
|
+
p.separator "Options:"
|
69
|
+
p.separator ""
|
70
|
+
|
71
|
+
p.on("-d", "--database PATH",
|
72
|
+
"Path to database file (#{options.database_path})") do |database|
|
73
|
+
options.database_path = database
|
74
|
+
end
|
75
|
+
|
76
|
+
p.on_tail("-v", "--version", "Show version") do
|
77
|
+
puts Melon.version_string
|
78
|
+
exit 0
|
79
|
+
end
|
80
|
+
|
81
|
+
p.on_tail("-h", "--help", "This is it") do
|
82
|
+
puts p
|
83
|
+
exit 0
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
begin
|
89
|
+
parser.order!(arguments)
|
90
|
+
rescue OptionParser::ParseError => e
|
91
|
+
CLI.error e
|
92
|
+
end
|
93
|
+
|
94
|
+
options
|
95
|
+
end
|
96
|
+
|
97
|
+
def format_command(name, desc, margin = 4, width = 22, wrapdesc = 80)
|
98
|
+
pad = "\n" + ' ' * width
|
99
|
+
desc = wrap_text(desc, wrapdesc - width).split("\n").join(pad)
|
100
|
+
|
101
|
+
' ' * margin + "#{name.ljust(width-margin)}#{desc}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def wrap_text(txt, col = 80)
|
105
|
+
txt.gsub(/(.{1,#{col}})( +|$\n?)|(.{1,#{col}})/,
|
106
|
+
"\\1\\3\n")
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
def self.error(error_obj_or_str, code = 1)
|
111
|
+
if error_obj_or_str.respond_to?('to_s')
|
112
|
+
error_str = error_obj_or_str.to_s
|
113
|
+
else
|
114
|
+
error_str = error_obj_or_str.inspect
|
115
|
+
end
|
116
|
+
|
117
|
+
$stderr.puts "melon: #{error_str}"
|
118
|
+
exit code
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
require 'ftools'
|
3
|
+
|
4
|
+
require 'melon/hasher'
|
5
|
+
require 'melon/cli'
|
6
|
+
|
7
|
+
# TODO: in commands, parse arguments with parse! in CLI, parse with order!
|
8
|
+
|
9
|
+
module Melon
|
10
|
+
module Commands
|
11
|
+
# needs a 'verify' command to check integrity of database
|
12
|
+
# needs a 'remove' command, or some way to deal with deletes/renames
|
13
|
+
class Base
|
14
|
+
attr_accessor :args, :options
|
15
|
+
attr_reader :description
|
16
|
+
|
17
|
+
def initialize(args, options)
|
18
|
+
self.args = args
|
19
|
+
self.options = options
|
20
|
+
end
|
21
|
+
|
22
|
+
def parser
|
23
|
+
raise
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.description
|
27
|
+
raise
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_options!
|
31
|
+
begin
|
32
|
+
parser.parse!(args)
|
33
|
+
rescue OptionParser::ParseError => e
|
34
|
+
CLI.error "#{self.class.to_s.split("::").last.downcase}: #{e}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
class Add < Base
|
41
|
+
|
42
|
+
def self.description
|
43
|
+
"Add files to the melon database"
|
44
|
+
end
|
45
|
+
|
46
|
+
def parser
|
47
|
+
@parser ||= OptionParser.new do |p|
|
48
|
+
p.banner = "Usage: melon add [options] file [file [file ...]]"
|
49
|
+
p.separator ""
|
50
|
+
p.separator Add.description
|
51
|
+
|
52
|
+
p.separator ""
|
53
|
+
p.separator "Options:"
|
54
|
+
p.separator ""
|
55
|
+
|
56
|
+
# p.on("-f", "--force",
|
57
|
+
# "Force the recalculation of the path that",
|
58
|
+
# " already exists in the database") do
|
59
|
+
# options.force = true
|
60
|
+
# end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def run
|
65
|
+
parse_options!
|
66
|
+
|
67
|
+
options.database.transaction do
|
68
|
+
args.each do |arg|
|
69
|
+
filename = File.expand_path(arg)
|
70
|
+
|
71
|
+
if File.directory?(filename)
|
72
|
+
CLI.error "argument is a directory: #{arg}"
|
73
|
+
end
|
74
|
+
|
75
|
+
if options.database[:by_path][filename]# and !options.force
|
76
|
+
CLI.error "path already present in database: #{arg}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# hash strategy should be encapsulated, ergo indirection here
|
80
|
+
hash = Hasher.digest(filename)
|
81
|
+
|
82
|
+
if options.database[:by_hash][hash]
|
83
|
+
CLI.error "file exists elsewhere in the database: #{arg}"
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
options.database[:by_hash][hash] = filename
|
88
|
+
options.database[:by_path][filename] = hash
|
89
|
+
puts "#{hash}:#{filename}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class Check < Base
|
96
|
+
def self.description
|
97
|
+
"Determine whether or not a copy of a file resides in the database"
|
98
|
+
end
|
99
|
+
|
100
|
+
def parser
|
101
|
+
@parser ||= OptionParser.new do |p|
|
102
|
+
p.banner = "Usage: melon check file [file [file ...]]"
|
103
|
+
p.separator ""
|
104
|
+
p.separator Check.description
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def run
|
109
|
+
parse_options!
|
110
|
+
|
111
|
+
options.database.transaction do
|
112
|
+
args.each do |filename|
|
113
|
+
hash = Hasher.digest(filename)
|
114
|
+
unless options.database[:by_hash][hash]
|
115
|
+
puts File.expand_path(filename)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Melon
|
2
|
+
module Commands
|
3
|
+
module BasicCommand
|
4
|
+
|
5
|
+
def execute(arguments, options)
|
6
|
+
new(arguments, options).run
|
7
|
+
end
|
8
|
+
|
9
|
+
# returns name {padding} description, for usage"
|
10
|
+
def short_usage(padding=" ")
|
11
|
+
name = Melon::Commands.translate_command(self)
|
12
|
+
extra_spaces = Melon::Commands.commands.collect do |c|
|
13
|
+
c.length
|
14
|
+
end.max - name.length
|
15
|
+
"#{name}#{padding}#{' ' * extra_spaces}#{self.description}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'melon/commands/basic_command'
|
2
|
+
|
3
|
+
module Melon
|
4
|
+
module Commands
|
5
|
+
class Help
|
6
|
+
# Help is a basic command that also ties itself in to cli#usage, so
|
7
|
+
# it's a little bit unconventional. Retrospectively, I should not
|
8
|
+
# have written it first.
|
9
|
+
extend BasicCommand
|
10
|
+
|
11
|
+
def self.description
|
12
|
+
"Get help with a specific command, or with Melon in general"
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :arguments
|
16
|
+
|
17
|
+
def initialize(arguments, options)
|
18
|
+
self.arguments = arguments
|
19
|
+
end
|
20
|
+
|
21
|
+
def parser
|
22
|
+
@parser ||= OptionParser.new do |opts|
|
23
|
+
Melon::Commands.command_hash.each do |name, command|
|
24
|
+
next if command == self.class
|
25
|
+
# TODO help banner: gem help help
|
26
|
+
# TODO flesh out parsing - give short_usage for each command
|
27
|
+
opts.on(name) { command.parser }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def run
|
33
|
+
begin
|
34
|
+
parser.parse!(arguments)
|
35
|
+
rescue OptionParser::InvalidOption => e
|
36
|
+
puts "melon: #{e.to_s}"
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
|
40
|
+
if arguments == ['help']
|
41
|
+
puts parser
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
|
45
|
+
# if arguments are empty, we handled it in CLI
|
46
|
+
puts "melon: '#{arguments.join(' ')}' is not a recognized command."
|
47
|
+
exit 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'subcommand'
|
3
|
+
|
4
|
+
require 'melon/version'
|
5
|
+
|
6
|
+
module Melon
|
7
|
+
class CLI
|
8
|
+
def self.execute(arguments=[])
|
9
|
+
new(arguments).run
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :arguments
|
13
|
+
|
14
|
+
def initialize(arguments)
|
15
|
+
self.arguments = arguments
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
# command may be nil, if no command is given
|
20
|
+
command = parser.parse!(arguments)
|
21
|
+
end
|
22
|
+
|
23
|
+
def parser
|
24
|
+
@parser ||= OptionParser.new do |opts|
|
25
|
+
opts.banner = "Example of what option parsing should look like"
|
26
|
+
|
27
|
+
opts.on("-d", "--database=PATH", String,
|
28
|
+
"Path to Melon's sqlite database.",
|
29
|
+
" (default ~/.melon/melon.db)") do |arg|
|
30
|
+
self.options[:database] = arg
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on("-v", "--version",
|
34
|
+
"Display the program version.") { puts "0.0.0" }
|
35
|
+
opts.on("-h", "--help",
|
36
|
+
"Show this help message.") { puts parser }
|
37
|
+
opts.separator ""
|
38
|
+
|
39
|
+
opts.command :create, "description text" do |subopts|
|
40
|
+
subopts.banner "create: create a directory, somewhere"
|
41
|
+
|
42
|
+
subopts.on("-f" "--force",
|
43
|
+
"Force creation.") { self.options[:force] = true }
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
opts.separator "To see a list of commands, use 'melon help commands'."
|
48
|
+
opts.separator "To get help with a specific command," +
|
49
|
+
" use 'melon help COMMAND'."
|
50
|
+
end
|
51
|
+
|
52
|
+
# OptionParser#command adds lazy option parsers to an array
|
53
|
+
# of subcommands
|
54
|
+
#
|
55
|
+
# melon help COMMAND prints the appropriate option
|
56
|
+
#
|
57
|
+
# PARSING:
|
58
|
+
# the option parser knows what the commands are at parse time
|
59
|
+
# so, scan the parse!() argument for the first command and pivot on
|
60
|
+
# it
|
61
|
+
#
|
62
|
+
# pre-cmd goes to the main option_parser.parse
|
63
|
+
# cmd goes into a string, is returned
|
64
|
+
# post-cmd goes to cmd parser.parse!()
|
65
|
+
#
|
66
|
+
# possibly implement that behavior for order! as well
|
67
|
+
end
|
68
|
+
end
|
data/lib/melon/hasher.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'digestif/hasher'
|
3
|
+
|
4
|
+
module Melon
|
5
|
+
class Hasher
|
6
|
+
def self.digest(filename)
|
7
|
+
hasher = Digestif::Hasher.new(filename)
|
8
|
+
hasher.options.read_size = 40000
|
9
|
+
hasher.options.seek_size = 80000000
|
10
|
+
hasher.digest
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/melon.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
lib = File.expand_path('../lib/', __FILE__)
|
2
|
+
$:.unshift lib unless $:.include?(lib)
|
3
|
+
require 'melon/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = %q{melon}
|
7
|
+
s.version = Melon.version
|
8
|
+
s.summary = %q{A media catalog}
|
9
|
+
s.description = %q{A tool for tracking files based on content, aiming to scale to arbitrarily large files without slowing down}
|
10
|
+
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
13
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
14
|
+
s.default_executable = 'melon'
|
15
|
+
|
16
|
+
s.require_path = 'lib'
|
17
|
+
|
18
|
+
s.has_rdoc = false
|
19
|
+
|
20
|
+
s.authors = ["Andrew Roberts"]
|
21
|
+
s.email = %q{adroberts@gmail.com}
|
22
|
+
s.homepage = "http://github.com/aroberts/melon"
|
23
|
+
|
24
|
+
s.add_dependency("digestif", ">=1.0.4")
|
25
|
+
|
26
|
+
s.add_development_dependency('cucumber')
|
27
|
+
s.add_development_dependency('aruba')
|
28
|
+
|
29
|
+
s.platform = Gem::Platform::RUBY
|
30
|
+
s.rubygems_version = %q{1.2.0}
|
31
|
+
end
|
32
|
+
|
data/script/console
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# File: script/console
|
3
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
4
|
+
|
5
|
+
libs = " -r irb/completion"
|
6
|
+
libs << " -r irb/ext/save-history"
|
7
|
+
|
8
|
+
# Perhaps use a console_lib to store any extra methods I may want available in the cosole
|
9
|
+
# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
|
10
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/melon.rb'}"
|
11
|
+
puts "Loading melon gem"
|
12
|
+
exec "#{irb} #{libs} --simple-prompt"
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'melon/database'
|
2
|
+
|
3
|
+
describe Melon::Database, "when passing a file that doesn't exist" do
|
4
|
+
before do
|
5
|
+
@database = Melon::Database.new(nonexistant_database_file)
|
6
|
+
end
|
7
|
+
|
8
|
+
after do
|
9
|
+
FileUtils.rm_rf(@database.path)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should be created" do
|
13
|
+
File.exist?(@database.path).should be_true
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
describe Melon::Database, "when passing a file that is not a database" do
|
19
|
+
before do
|
20
|
+
@file = non_database_file
|
21
|
+
end
|
22
|
+
|
23
|
+
after do
|
24
|
+
FileUtils.rm_rf(@file)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should throw an exception" do
|
28
|
+
lambda {
|
29
|
+
@database = Melon::Database.new(non_database_file)
|
30
|
+
}.should raise_error(ActiveRecord::StatementInvalid)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'melon/fingerprint'
|
2
|
+
require 'melon/database' # may need to reverse the order of these
|
3
|
+
|
4
|
+
|
5
|
+
describe Fingerprint, "when creating with just a file" do
|
6
|
+
before do
|
7
|
+
File.stub!(:exist?) { true }
|
8
|
+
|
9
|
+
@database = Database.new(nonexistant_database_file)
|
10
|
+
@fingerprint = Fingerprint.new(:file => small_media_file)
|
11
|
+
@fingerprint.save!
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
FileUtils.rm_rf(@database.path)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should resolve the path" do
|
19
|
+
@fingerprint.file.should match(/^\//)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should be valid" do
|
23
|
+
@fingerprint.should be_valid
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should set the digest field" do
|
27
|
+
@fingerprint.digest.should_not be_nil
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'melon/parsed_arguments'
|
2
|
+
|
3
|
+
describe Melon::ParsedArguments, "when splitting valid arguments" do
|
4
|
+
before do
|
5
|
+
@args = Melon::ParsedArguments.new(%w(-t -b help plate /path/to/file -t 5))
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should correctly find the program arguments" do
|
9
|
+
@args.program_arguments.should include("-t", "-b")
|
10
|
+
@args.program_arguments.length.should == 2
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should correctly find the command" do
|
14
|
+
@args.command.should == "help"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should correctly find the command arguments" do
|
18
|
+
@args.command_arguments.should include("plate", "/path/to/file", "-t", "5")
|
19
|
+
@args.command_arguments.length.should == 4
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe Melon::ParsedArguments, "when splitting argument-less commands" do
|
24
|
+
before do
|
25
|
+
@args = Melon::ParsedArguments.new(%w(help))
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should correctly find the program arguments" do
|
29
|
+
@args.program_arguments.should be_empty
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should correctly find the command" do
|
33
|
+
@args.command.should == "help"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should correctly find the command arguments" do
|
37
|
+
@args.command_arguments.should be_empty
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe Melon::ParsedArguments, "when splitting command-less arguments" do
|
42
|
+
before do
|
43
|
+
@args = Melon::ParsedArguments.new(%w(--help))
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should set the program arguments" do
|
47
|
+
@args.program_arguments.should include("--help")
|
48
|
+
@args.program_arguments.length.should == 1
|
49
|
+
end
|
50
|
+
|
51
|
+
it "shound not set a command" do
|
52
|
+
@args.command.should be_nil
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should not set any command arguments" do
|
56
|
+
@args.command_arguments.should be_nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe Melon::ParsedArguments, "when splitting invalid commands" do
|
61
|
+
before do
|
62
|
+
@args = Melon::ParsedArguments.new(%w(fizzle))
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should not set a command" do
|
66
|
+
@args.command.should be_nil
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should treat the invalid command as a program argument" do
|
70
|
+
@args.program_arguments.should include("fizzle")
|
71
|
+
@args.program_arguments.length.should == 1
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should not set command arguments" do
|
75
|
+
@args.command_arguments.should be_nil
|
76
|
+
end
|
77
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
begin
|
2
|
+
require 'rspec'
|
3
|
+
rescue LoadError
|
4
|
+
require 'rubygems' unless ENV['NO_RUBYGEMS']
|
5
|
+
gem 'rspec'
|
6
|
+
require 'rspec'
|
7
|
+
end
|
8
|
+
|
9
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
10
|
+
require 'melon'
|
11
|
+
include Melon
|
12
|
+
|
13
|
+
|
14
|
+
def nonexistant_database_file
|
15
|
+
'tmp/' + (0..8).map{ ('a'..'z').to_a[rand(26)] }.join + '.db'
|
16
|
+
end
|
17
|
+
|
18
|
+
def non_database_file
|
19
|
+
name = nonexistant_database_file
|
20
|
+
File.open(name, 'w') {|f| f.write("bad") }
|
21
|
+
name
|
22
|
+
end
|
23
|
+
|
24
|
+
def small_media_file
|
25
|
+
name = nonexistant_database_file
|
26
|
+
File.open(name, 'w') do |f|
|
27
|
+
f.write(<<EOS
|
28
|
+
<p>
|
29
|
+
It is included in the Ruby standard library.
|
30
|
+
</p>
|
31
|
+
<h2>Description</h2>
|
32
|
+
<p>
|
33
|
+
ftools adds several (class, not instance) methods to the <a
|
34
|
+
href="File.html">File</a> class, for copying, moving, deleting, installing,
|
35
|
+
and comparing files, as well as creating a directory <a
|
36
|
+
href="File.html#M002554">path</a>. See the <a href="File.html">File</a>
|
37
|
+
class for details.
|
38
|
+
</p>
|
39
|
+
<p>
|
40
|
+
<a href="FileUtils.html">FileUtils</a> contains all or nearly all the same
|
41
|
+
functionality and more, and is a recommended option over ftools
|
42
|
+
</p>
|
43
|
+
<p>
|
44
|
+
When you
|
45
|
+
</p>
|
46
|
+
<pre>
|
47
|
+
require 'ftools'
|
48
|
+
</pre>
|
49
|
+
<p>
|
50
|
+
then the <a href="File.html">File</a> class aquires some utility methods
|
51
|
+
for copying, moving, and deleting files, and more.
|
52
|
+
</p>
|
53
|
+
<p>
|
54
|
+
See the method descriptions below, and consider using <a
|
55
|
+
href="FileUtils.html">FileUtils</a> as it is more comprehensive.
|
56
|
+
</p>
|
57
|
+
EOS
|
58
|
+
)
|
59
|
+
end
|
60
|
+
name
|
61
|
+
end
|
metadata
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: melon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Andrew Roberts
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-25 00:00:00 -05:00
|
19
|
+
default_executable: melon
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: digestif
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 31
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 0
|
33
|
+
- 4
|
34
|
+
version: 1.0.4
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: cucumber
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 3
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
version: "0"
|
49
|
+
type: :development
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: aruba
|
53
|
+
prerelease: false
|
54
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
type: :development
|
64
|
+
version_requirements: *id003
|
65
|
+
description: A tool for tracking files based on content, aiming to scale to arbitrarily large files without slowing down
|
66
|
+
email: adroberts@gmail.com
|
67
|
+
executables:
|
68
|
+
- melon
|
69
|
+
extensions: []
|
70
|
+
|
71
|
+
extra_rdoc_files: []
|
72
|
+
|
73
|
+
files:
|
74
|
+
- .gitignore
|
75
|
+
- Gemfile
|
76
|
+
- History.txt
|
77
|
+
- README.textile
|
78
|
+
- Rakefile
|
79
|
+
- TODO
|
80
|
+
- bin/melon
|
81
|
+
- features/basic.feature
|
82
|
+
- features/edges.feature
|
83
|
+
- features/step_definitions/basic_steps.rb
|
84
|
+
- features/support/env.rb
|
85
|
+
- lib/melon.rb
|
86
|
+
- lib/melon/cli.rb
|
87
|
+
- lib/melon/commands.rb
|
88
|
+
- lib/melon/commands/add.rb
|
89
|
+
- lib/melon/commands/basic_command.rb
|
90
|
+
- lib/melon/commands/help.rb
|
91
|
+
- lib/melon/example.rb
|
92
|
+
- lib/melon/hasher.rb
|
93
|
+
- lib/melon/version.rb
|
94
|
+
- melon.gemspec
|
95
|
+
- script/console
|
96
|
+
- spec/melon/cli_spec.rb
|
97
|
+
- spec/melon/database_spec.rb
|
98
|
+
- spec/melon/fingerprint_spec.rb
|
99
|
+
- spec/melon/parsed_arguments_spec.rb
|
100
|
+
- spec/spec_helper.rb
|
101
|
+
has_rdoc: true
|
102
|
+
homepage: http://github.com/aroberts/melon
|
103
|
+
licenses: []
|
104
|
+
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
hash: 3
|
116
|
+
segments:
|
117
|
+
- 0
|
118
|
+
version: "0"
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
hash: 3
|
125
|
+
segments:
|
126
|
+
- 0
|
127
|
+
version: "0"
|
128
|
+
requirements: []
|
129
|
+
|
130
|
+
rubyforge_project:
|
131
|
+
rubygems_version: 1.3.7
|
132
|
+
signing_key:
|
133
|
+
specification_version: 3
|
134
|
+
summary: A media catalog
|
135
|
+
test_files:
|
136
|
+
- features/basic.feature
|
137
|
+
- features/edges.feature
|
138
|
+
- features/step_definitions/basic_steps.rb
|
139
|
+
- features/support/env.rb
|
140
|
+
- spec/melon/cli_spec.rb
|
141
|
+
- spec/melon/database_spec.rb
|
142
|
+
- spec/melon/fingerprint_spec.rb
|
143
|
+
- spec/melon/parsed_arguments_spec.rb
|
144
|
+
- spec/spec_helper.rb
|