file_scheduler 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/Gemfile +9 -0
- data/Guardfile +8 -0
- data/Rakefile +10 -0
- data/bin/file-scheduler +19 -0
- data/file_scheduler.gemspec +24 -0
- data/lib/file_scheduler.rb +21 -0
- data/lib/file_scheduler/attributes_parser.rb +40 -0
- data/lib/file_scheduler/base.rb +62 -0
- data/lib/file_scheduler/content.rb +19 -0
- data/lib/file_scheduler/core_ext.rb +37 -0
- data/lib/file_scheduler/file.rb +80 -0
- data/lib/file_scheduler/log.rb +53 -0
- data/lib/file_scheduler/playlist.rb +36 -0
- data/lib/file_scheduler/scheduling.rb +59 -0
- data/lib/file_scheduler/time_chain.rb +19 -0
- data/lib/file_scheduler/time_interval.rb +38 -0
- data/lib/file_scheduler/time_mark.rb +66 -0
- data/lib/file_scheduler/time_parser.rb +37 -0
- data/lib/file_scheduler/url.rb +56 -0
- data/lib/file_scheduler/version.rb +3 -0
- data/spec/lib/file_scheduler/attributes_parser_spec.rb +33 -0
- data/spec/lib/file_scheduler/base_spec.rb +75 -0
- data/spec/lib/file_scheduler/content_shared_examples.rb +12 -0
- data/spec/lib/file_scheduler/file_spec.rb +104 -0
- data/spec/lib/file_scheduler/log_spec.rb +50 -0
- data/spec/lib/file_scheduler/playlist_spec.rb +17 -0
- data/spec/lib/file_scheduler/scheduling_spec.rb +114 -0
- data/spec/lib/file_scheduler/time_interval_spec.rb +46 -0
- data/spec/lib/file_scheduler/time_mark_spec.rb +51 -0
- data/spec/lib/file_scheduler/time_parser_spec.rb +33 -0
- data/spec/lib/file_scheduler/url_spec.rb +33 -0
- data/spec/lib/file_scheduler_spec.rb +117 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/pathname.rb +23 -0
- data/spec/support/time.rb +19 -0
- data/spec/support/tmpdir.rb +1 -0
- metadata +118 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
module FileScheduler
|
2
|
+
class Scheduling
|
3
|
+
|
4
|
+
attr_accessor :root, :time, :log
|
5
|
+
|
6
|
+
def initialize(root, time)
|
7
|
+
@root = root
|
8
|
+
@time = time
|
9
|
+
end
|
10
|
+
|
11
|
+
def log
|
12
|
+
@log ||= FileScheduler::Log.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def schedulable?(content)
|
16
|
+
schedulable_by_time?(content) and
|
17
|
+
schedulable_by_repeat?(content)
|
18
|
+
end
|
19
|
+
|
20
|
+
def schedulable_by_time?(content)
|
21
|
+
content.time_constraints.nil? or
|
22
|
+
content.time_constraints.matches?(time)
|
23
|
+
end
|
24
|
+
|
25
|
+
def schedulable_by_repeat?(content)
|
26
|
+
content.repeat_constraints.nil? or
|
27
|
+
log.distance(content).nil? or
|
28
|
+
log.distance(content) >= content.repeat_constraints
|
29
|
+
end
|
30
|
+
|
31
|
+
def schedulable_contents
|
32
|
+
@schedulable_contents ||= root.contents.select do |content|
|
33
|
+
schedulable? content
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def forced_contents
|
38
|
+
schedulable_contents.select &:forced_started_time?
|
39
|
+
end
|
40
|
+
|
41
|
+
def forced_next
|
42
|
+
forced_contents.first
|
43
|
+
end
|
44
|
+
|
45
|
+
def schedulable_next
|
46
|
+
schedulable_contents.shuffle.first
|
47
|
+
end
|
48
|
+
|
49
|
+
def next_without_log
|
50
|
+
forced_next or schedulable_next
|
51
|
+
end
|
52
|
+
|
53
|
+
def next_with_log
|
54
|
+
log.log(next_without_log)
|
55
|
+
end
|
56
|
+
alias_method :next, :next_with_log
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module FileScheduler
|
2
|
+
class TimeChain
|
3
|
+
|
4
|
+
attr_reader :parts
|
5
|
+
|
6
|
+
def initialize(*parts)
|
7
|
+
@parts = parts.flatten
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
other.respond_to?(:parts) and parts == other.parts
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
parts.join('/')
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module FileScheduler
|
4
|
+
class TimeInterval
|
5
|
+
|
6
|
+
attr_reader :from, :to
|
7
|
+
|
8
|
+
def initialize(from, to)
|
9
|
+
@from, @to = from, to
|
10
|
+
end
|
11
|
+
|
12
|
+
def include?(time)
|
13
|
+
if from > to
|
14
|
+
not time.between?(to, from)
|
15
|
+
else
|
16
|
+
time.between?(from, to)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
alias_method :matches?, :include?
|
21
|
+
|
22
|
+
def ==(other)
|
23
|
+
[:from, :to].all? do |attribute|
|
24
|
+
other.respond_to?(attribute) and send(attribute) == other.send(attribute)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
"#{from}-#{to}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class Time
|
2
|
+
alias_method :minute, :min
|
3
|
+
alias_method :week_day, :wday
|
4
|
+
|
5
|
+
alias_method :compare_to_without_time_mark_support, :<=>
|
6
|
+
|
7
|
+
def <=>(other)
|
8
|
+
if FileScheduler::TimeMark === other
|
9
|
+
-(other <=> self)
|
10
|
+
else
|
11
|
+
compare_to_without_time_mark_support other
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module FileScheduler
|
17
|
+
class TimeMark
|
18
|
+
|
19
|
+
attr_reader :attributes
|
20
|
+
|
21
|
+
def initialize(attributes = {})
|
22
|
+
@attributes = attributes
|
23
|
+
end
|
24
|
+
|
25
|
+
def matches?(time)
|
26
|
+
attributes.all? do |attribute, value|
|
27
|
+
time.send(attribute) == value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def [](attribute)
|
32
|
+
attributes[attribute]
|
33
|
+
end
|
34
|
+
|
35
|
+
[:year, :month, :day, :hour, :minute, :week_day].each do |attribute|
|
36
|
+
define_method(attribute) do
|
37
|
+
attributes[attribute]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def <=>(time)
|
42
|
+
[:year, :month, :day, :week_day, :hour, :minute].each do |attribute|
|
43
|
+
value = attributes[attribute]
|
44
|
+
if value
|
45
|
+
comparaison = value <=> time.send(attribute)
|
46
|
+
return comparaison unless comparaison == 0
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
0
|
51
|
+
end
|
52
|
+
|
53
|
+
include Comparable
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
[:year, :month, :day, :week_day, :hour, :minute].collect do |attribute|
|
57
|
+
value = attributes[attribute]
|
58
|
+
if value
|
59
|
+
suffix = attribute == :month ? "M" : attribute.to_s.chars.first
|
60
|
+
"#{value}#{suffix}"
|
61
|
+
end
|
62
|
+
end.join
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module FileScheduler
|
2
|
+
class TimeParser
|
3
|
+
|
4
|
+
@@time_mark_fields =
|
5
|
+
[["y", 4], ["M", 2], ["d", 2], ["w", 1], ["h", 2], ["m", 2]]
|
6
|
+
|
7
|
+
def self.time_mark_format
|
8
|
+
@@time_mark_fields.collect do |field|
|
9
|
+
"(\\d{#{field[1]}}#{field[0]})?"
|
10
|
+
end.join
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(string)
|
14
|
+
parsed_fields = string.scan(/^T?#{self.class.time_mark_format}(-#{self.class.time_mark_format})?/).first.map do |field|
|
15
|
+
field.to_i if field
|
16
|
+
end
|
17
|
+
|
18
|
+
from = time_mark(parsed_fields.first(6))
|
19
|
+
to = time_mark(parsed_fields.last(6))
|
20
|
+
|
21
|
+
if to
|
22
|
+
TimeInterval.new from, to
|
23
|
+
else
|
24
|
+
from
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def time_mark(parsed_fields)
|
29
|
+
unless parsed_fields.all?(&:nil?)
|
30
|
+
attributes = Hash[ [[:year, :month, :day, :week_day, :hour, :minute], parsed_fields].transpose ]
|
31
|
+
attributes.delete_if { |k,v| v.nil? }
|
32
|
+
TimeMark.new attributes
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module FileScheduler
|
2
|
+
class URL
|
3
|
+
include FileScheduler::Content
|
4
|
+
|
5
|
+
attr_accessor :path, :url
|
6
|
+
|
7
|
+
def initialize(attributes = {})
|
8
|
+
if String === attributes
|
9
|
+
attributes = { :path => attributes }
|
10
|
+
end
|
11
|
+
|
12
|
+
attributes.each { |k,v| send "#{k}=", v }
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
other.respond_to?(:path) and path == other.path
|
17
|
+
end
|
18
|
+
|
19
|
+
def hidden?
|
20
|
+
path_parts.any? do |part|
|
21
|
+
part.start_with?("_")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def path_parts
|
26
|
+
@path_parts ||= path.split("/")
|
27
|
+
end
|
28
|
+
|
29
|
+
def name
|
30
|
+
@path_parts.last
|
31
|
+
end
|
32
|
+
|
33
|
+
def time_constraints
|
34
|
+
part_constraints = path_parts.collect do |part|
|
35
|
+
parser.parse(part)
|
36
|
+
end.compact
|
37
|
+
|
38
|
+
if part_constraints.size > 1
|
39
|
+
FileScheduler::TimeChain.new part_constraints
|
40
|
+
else
|
41
|
+
part_constraints.first
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
(url and url.to_s) or path
|
47
|
+
end
|
48
|
+
|
49
|
+
def attributes
|
50
|
+
@attributes ||= path_parts.inject({}) do |attributes, part|
|
51
|
+
attributes.merge attributes_parser.parse(part)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FileScheduler::AttributesParser do
|
4
|
+
|
5
|
+
it "should be empty without {}" do
|
6
|
+
subject.parse("dummy").should be_empty
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should be empty when {} is empty" do
|
10
|
+
subject.parse("dummy{}").should be_empty
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should use {...} at the end of name" do
|
14
|
+
subject.parse("dummy{key=value}").should == { :key => "value" }
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should use {...} before file extension" do
|
18
|
+
subject.parse("dummy{key=value}.wav").should == { :key => "value" }
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should not use {...} before a simple dot" do
|
22
|
+
subject.parse("dummy{key=value}.otherpart.wav").should be_empty
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should support several attributes" do
|
26
|
+
subject.parse("dummy{key1=value1,key2=value2}").should == { :key1 => "value1", :key2 => "value2" }
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should strip spaces" do
|
30
|
+
subject.parse("dummy{ key1 = value1 , key2 = value2 }").should == { :key1 => "value1", :key2 => "value2" }
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FileScheduler::Base do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
subject.root = mock(:contents => mock)
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:content) { mock }
|
10
|
+
|
11
|
+
describe "#contents" do
|
12
|
+
|
13
|
+
it "should return root contents" do
|
14
|
+
subject.contents.should == subject.root.contents
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "scheduling" do
|
20
|
+
|
21
|
+
let(:time) { Time.now }
|
22
|
+
|
23
|
+
it "should return a Scheduling with specified time" do
|
24
|
+
subject.scheduling(time).time.should == time
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should return a Scheduling with the same root" do
|
28
|
+
subject.scheduling.root.should == subject.root
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should return a Scheduling with the same log" do
|
32
|
+
subject.scheduling.log.should == subject.log
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "next" do
|
38
|
+
|
39
|
+
let(:next_content) { content }
|
40
|
+
|
41
|
+
before(:each) do
|
42
|
+
subject.stub :scheduling => mock(:next => content)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should return the next content choosed by scheduling" do
|
46
|
+
subject.next.should == next_content
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should invoke after_next with selected content" do
|
50
|
+
subject.should_receive(:after_next).with(next_content)
|
51
|
+
subject.next
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "forced_next" do
|
57
|
+
|
58
|
+
let(:forced_content) { content }
|
59
|
+
|
60
|
+
before(:each) do
|
61
|
+
subject.stub :scheduling => mock(:forced_next => content)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should return the forced content choosed by scheduling" do
|
65
|
+
subject.forced_next.should == forced_content
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should invoke after_next with selected content" do
|
69
|
+
subject.should_receive(:after_next).with(forced_content)
|
70
|
+
subject.forced_next
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FileScheduler::File do
|
4
|
+
|
5
|
+
subject { file "dummy" }
|
6
|
+
|
7
|
+
def file(file, parent = nil)
|
8
|
+
FileScheduler::File.new file, parent
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#name" do
|
12
|
+
|
13
|
+
it "should use path basename" do
|
14
|
+
file("/path/to/basename").name.should == "basename"
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#local_time_constraints" do
|
20
|
+
|
21
|
+
it "should parse the time specifications in the name" do
|
22
|
+
file("/path/to/12h30m-dummy",mock).local_time_constraints.should == FileScheduler::TimeMark.new(:hour => 12, :minute => 30)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should be nil when parent isn't defined (root directory)" do
|
26
|
+
file("/path/to/12h30m-dummy").local_time_constraints.should be_nil
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#time_constraints" do
|
32
|
+
|
33
|
+
it "should concat with parent constraints" do
|
34
|
+
parent = mock(:time_constraints => FileScheduler::TimeMark.new(:week_day => 1))
|
35
|
+
file("/path/1w/12h30m-dummy", parent).time_constraints.should == FileScheduler::TimeChain.new(parent.time_constraints, FileScheduler::TimeMark.new(:hour => 12, :minute => 30))
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "file_system_children" do
|
41
|
+
|
42
|
+
it "should return pathnames of files and directories present under file" do
|
43
|
+
Dir.mktmpdir do |base|
|
44
|
+
base = Pathname.new(base)
|
45
|
+
|
46
|
+
file = base.touch "file"
|
47
|
+
directory = base.mkpath "directory"
|
48
|
+
|
49
|
+
file(base).file_system_children.should include(file, directory)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "children" do
|
56
|
+
|
57
|
+
it "should return children created from files and directories present under path" do
|
58
|
+
Dir.mktmpdir do |base|
|
59
|
+
base = Pathname.new(base)
|
60
|
+
|
61
|
+
file = base.touch "file"
|
62
|
+
directory = base.mkpath "directory"
|
63
|
+
|
64
|
+
parent = file(base)
|
65
|
+
parent.children.should include(file(file, parent))
|
66
|
+
parent.children.should include(file(directory, parent))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should ignore files prefixed with underscore" do
|
71
|
+
Dir.mktmpdir do |base|
|
72
|
+
base = Pathname.new(base)
|
73
|
+
|
74
|
+
file = base.touch "_ignored_file"
|
75
|
+
directory = base.mkpath "_ignored_directory"
|
76
|
+
|
77
|
+
file(base).children.should be_empty
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "local_attributes" do
|
84
|
+
|
85
|
+
it "should use attributes found in name" do
|
86
|
+
file("dummy{key=value}.wav").local_attributes.should == { :key => "value" }
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#attributes" do
|
92
|
+
|
93
|
+
it "should merge parent attributes with local attributes" do
|
94
|
+
parent = mock(:attributes => {:key1 => "parent_value1", :key2 => "value2"})
|
95
|
+
file("dummy{key1=value1}.wav",parent).attributes.should == { :key1 => "value1", :key2 => "value2" }
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
require File.expand_path("../content_shared_examples", __FILE__)
|
101
|
+
|
102
|
+
it_behaves_like "a content"
|
103
|
+
|
104
|
+
end
|