textrepo 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +40 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +77 -0
- data/Rakefile +37 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/rbnotes/lib/rbnotes.rb +8 -0
- data/examples/rbnotes/lib/rbnotes/commands.rb +118 -0
- data/examples/rbnotes/lib/rbnotes/commands/import.rb +62 -0
- data/examples/rbnotes/lib/rbnotes/commands/list.rb +60 -0
- data/examples/rbnotes/lib/rbnotes/commands/show.rb +25 -0
- data/examples/rbnotes/lib/rbnotes/version.rb +4 -0
- data/examples/rbnotes/rbnotes +33 -0
- data/lib/textrepo.rb +6 -0
- data/lib/textrepo/error.rb +50 -0
- data/lib/textrepo/file_system_repository.rb +148 -0
- data/lib/textrepo/repository.rb +62 -0
- data/lib/textrepo/timestamp.rb +63 -0
- data/lib/textrepo/version.rb +3 -0
- data/textrepo.gemspec +29 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4d26af6dd86e8b79c9e50300f8c31102bf79d5c751e175740a7225c2ca4a9b33
|
4
|
+
data.tar.gz: 8d398c0848c0a4b06d2a775f3cb6bf1f49489be7f5ba5d8cf7eab2aad068b5c7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ef79855cd99ebc3baea4368c00cdc4d439478cfc9733d45eee1e7a08ca005bf0dc15824cb57b46326c982e724acc762b25b722c112f19ae4f67349d575fffdcb
|
7
|
+
data.tar.gz: 4af657999971e6d3cef41dc5d3ef7ea1017a536b772372d38e79ffb91196c239585a0c932fe10e42f9d7ccbfee8fa477ec31bd97afa9cd4e1c31f0c894772110
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Change Log
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/)
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
6
|
+
|
7
|
+
## [Unreleased]
|
8
|
+
Nothing to record here.
|
9
|
+
|
10
|
+
## [0.4.0] - 2020-10-14
|
11
|
+
### Added
|
12
|
+
- Released to rubygems.org.
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
- Rename the method, `Repository#notes` to `entries`.
|
16
|
+
- Modify the instruction to install
|
17
|
+
|
18
|
+
## [0.3.0] - 2020-10-11
|
19
|
+
### Added
|
20
|
+
- Go public onto GitHub.
|
21
|
+
- Add an example (`rbnotes`) to demonstrate how to use `textrepo`.
|
22
|
+
|
23
|
+
### Changed
|
24
|
+
- Modify not to handle fraction of time in Timestamp.
|
25
|
+
- Instead, Timestamp can have a suffix to distinguish 2 stamps those
|
26
|
+
represent the same time.
|
27
|
+
|
28
|
+
## [0.2.0] - 2020-09-28
|
29
|
+
### Changed
|
30
|
+
- Modify to handle millisecond in Timestamp
|
31
|
+
|
32
|
+
## [0.1.0] - 2020-09-23
|
33
|
+
### Added
|
34
|
+
- Add some target in Rakefile to run test easily.
|
35
|
+
- Add error classes
|
36
|
+
- Add Timestamp class, it will be an identifier of each text in the repo.
|
37
|
+
- Add Repository class (an abstract base class for concrete repository
|
38
|
+
implementations)
|
39
|
+
- Implement API for FileSystemRepository class (create/read/update/delete).
|
40
|
+
- Add tests.
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
textrepo (0.4.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
minitest (5.14.2)
|
10
|
+
rake (13.0.1)
|
11
|
+
|
12
|
+
PLATFORMS
|
13
|
+
ruby
|
14
|
+
|
15
|
+
DEPENDENCIES
|
16
|
+
bundler (~> 2.1)
|
17
|
+
minitest (~> 5.0)
|
18
|
+
rake (~> 13.0)
|
19
|
+
textrepo!
|
20
|
+
|
21
|
+
BUNDLED WITH
|
22
|
+
2.1.4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 mnbi
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# Textrepo
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/mnbi/textrepo.svg?branch=main)](https://travis-ci.org/mnbi/textrepo)
|
4
|
+
|
5
|
+
Textrepo is a repository to store a note with a timestamp. Each note
|
6
|
+
in the repository operates with the associated timestamp.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'textrepo'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle install
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install textrepo
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
Here is a very short sample to use `textrepo`. It will make
|
27
|
+
`~/textrepo_sample` directory and store some text into it.
|
28
|
+
|
29
|
+
``` ruby
|
30
|
+
#!/usr/bin/env ruby
|
31
|
+
|
32
|
+
require "textrepo"
|
33
|
+
|
34
|
+
conf = {
|
35
|
+
:repository_type => :file_system,
|
36
|
+
:repository_name => "textrepo_sample",
|
37
|
+
:repository_base => File.expand_path("~"),
|
38
|
+
}
|
39
|
+
|
40
|
+
repo = Textrepo.init(conf)
|
41
|
+
|
42
|
+
t0 = Time.now
|
43
|
+
|
44
|
+
stamps = []
|
45
|
+
stamps << repo.create(Textrepo::Timestamp.new(t0), ["jan", "feb", "mar"])
|
46
|
+
stamps << repo.create(Textrepo::Timestamp.new(t0, 1), ["apr", "may", "jun"])
|
47
|
+
stamps << repo.create(Textrepo::Timestamp.new(t0, 2), ["jul", "aug", "sep"])
|
48
|
+
stamps << repo.create(Textrepo::Timestamp.new(t0, 3), ["oct", "nov", "dec"])
|
49
|
+
|
50
|
+
entries = repo.notes
|
51
|
+
puts entries
|
52
|
+
|
53
|
+
stamps.each { |stamp|
|
54
|
+
text = repo.read(stamp)
|
55
|
+
puts "----"
|
56
|
+
puts stamp
|
57
|
+
puts text
|
58
|
+
}
|
59
|
+
```
|
60
|
+
|
61
|
+
Also see `examples` directory. There is a small tool to demonstrate
|
62
|
+
how to use `textrepo`.
|
63
|
+
|
64
|
+
## Development
|
65
|
+
|
66
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
67
|
+
|
68
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
69
|
+
|
70
|
+
## Contributing
|
71
|
+
|
72
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mnbi/textrepo.
|
73
|
+
|
74
|
+
|
75
|
+
## License
|
76
|
+
|
77
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << "test"
|
6
|
+
t.libs << "lib"
|
7
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
8
|
+
end
|
9
|
+
|
10
|
+
task :default => :test
|
11
|
+
|
12
|
+
desc 'Setup test data'
|
13
|
+
task :setup_test do
|
14
|
+
print "Setting up to execute tests..."
|
15
|
+
load('test/fixtures/setup_test_repo.rb', true)
|
16
|
+
puts "done."
|
17
|
+
end
|
18
|
+
|
19
|
+
task test: [:setup_test, :clean_sandbox]
|
20
|
+
|
21
|
+
require "fileutils"
|
22
|
+
|
23
|
+
desc "Clean up test/sandbox"
|
24
|
+
task :clean_sandbox do
|
25
|
+
sandbox = File.expand_path('test/sandbox', __dir__)
|
26
|
+
entries = Dir.entries(sandbox).filter_map { |e|
|
27
|
+
File.expand_path(e, sandbox) unless e == '.' or e == '..'
|
28
|
+
}
|
29
|
+
entries.each { |e|
|
30
|
+
next if File.basename(e) == '.gitkeep'
|
31
|
+
FileUtils.remove_entry_secure(e) if FileTest.exist?(e)
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
task :clobber => :clean_sandbox
|
36
|
+
CLOBBER << 'test/fixtures/notes'
|
37
|
+
CLOBBER << 'test/fixtures/test_repo'
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "textrepo"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
module Rbnotes
|
2
|
+
module Commands
|
3
|
+
class Command
|
4
|
+
def execute(args, conf)
|
5
|
+
Builtins::DEFAULT_CMD.new.execute(args, conf)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# Built-in commands:
|
10
|
+
# - repo: prints the absolute path of the repository.
|
11
|
+
# - conf: prints all of the current configuration settings.
|
12
|
+
# - stamp: converts given TIME_STR into a timestamp.
|
13
|
+
# - time: converts given STAMP into a time string.
|
14
|
+
module Builtins
|
15
|
+
class Help < Command
|
16
|
+
def execute(_, _)
|
17
|
+
puts <<USAGE
|
18
|
+
usage: rbnotes [command] [args]
|
19
|
+
|
20
|
+
command:
|
21
|
+
import FILE : import a FILE into the repository
|
22
|
+
list NUM : list NUM notes
|
23
|
+
show STAMP : show the note specified with STAMP
|
24
|
+
|
25
|
+
repo : print the repository path
|
26
|
+
stamp TIME_STR : convert TIME_STR into a timestamp
|
27
|
+
time STAMP : convert STAMP into a time string
|
28
|
+
version : print version
|
29
|
+
help : show help
|
30
|
+
USAGE
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Version < Command
|
35
|
+
def execute(_, _)
|
36
|
+
rbnotes_version = "rbnotes #{Rbnotes::VERSION} (#{Rbnotes::RELEASE})"
|
37
|
+
textrepo_version = "textrepo #{Textrepo::VERSION}"
|
38
|
+
puts "#{rbnotes_version} (#{textrepo_version})"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Repo < Command
|
43
|
+
def execute(_, conf)
|
44
|
+
name = conf[:repository_name]
|
45
|
+
base = conf[:repository_base]
|
46
|
+
type = conf[:repository_type]
|
47
|
+
|
48
|
+
puts case type
|
49
|
+
when :file_system
|
50
|
+
"#{base}/#{name}"
|
51
|
+
else
|
52
|
+
"#{base}/#{name}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Conf < Command
|
58
|
+
def execute(_, conf)
|
59
|
+
conf.keys.sort.each { |k|
|
60
|
+
puts "#{k}=#{conf[k]}"
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
require "time"
|
66
|
+
|
67
|
+
class Stamp < Command
|
68
|
+
def execute(args, _)
|
69
|
+
time_str = args.shift
|
70
|
+
unless time_str.nil?
|
71
|
+
puts Textrepo::Timestamp.new(::Time.parse(time_str)).to_s
|
72
|
+
else
|
73
|
+
puts "not specified TIME_STR"
|
74
|
+
super
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Time < Command
|
80
|
+
def execute(args, _)
|
81
|
+
stamp = args.shift
|
82
|
+
unless stamp.nil?
|
83
|
+
puts ::Time.new(*Textrepo::Timestamp.split_stamp(stamp).map(&:to_i)).to_s
|
84
|
+
else
|
85
|
+
puts "not specified STAMP"
|
86
|
+
super
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
DEFAULT_CMD = Help
|
92
|
+
end
|
93
|
+
|
94
|
+
DEFAULT_CMD_NAME = "help"
|
95
|
+
|
96
|
+
class << self
|
97
|
+
def load(cmd_name)
|
98
|
+
cmd_name ||= DEFAULT_CMD_NAME
|
99
|
+
klass_name = cmd_name.capitalize
|
100
|
+
|
101
|
+
klass = nil
|
102
|
+
if Builtins.const_defined?(klass_name, false)
|
103
|
+
klass = Builtins::const_get(klass_name, false)
|
104
|
+
else
|
105
|
+
begin
|
106
|
+
require_relative "commands/#{cmd_name}"
|
107
|
+
klass = const_get(klass_name, false)
|
108
|
+
rescue LoadError => _
|
109
|
+
STDERR.puts "unknown command: #{cmd_name}"
|
110
|
+
klass = Builtins::DEFAULT_CMD
|
111
|
+
end
|
112
|
+
end
|
113
|
+
klass.new
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Rbnotes
|
2
|
+
class Commands::Import < Commands::Command
|
3
|
+
def execute(args, conf)
|
4
|
+
file = args.shift
|
5
|
+
unless file.nil?
|
6
|
+
st = File::Stat.new(file)
|
7
|
+
btime = st.respond_to?(:birthtime) ? st.birthtime : st.mtime
|
8
|
+
stamp = Textrepo::Timestamp.new(btime)
|
9
|
+
puts "Import [%s] (timestamp [%s]) ..." % [file, stamp]
|
10
|
+
|
11
|
+
repo = Textrepo.init(conf)
|
12
|
+
content = nil
|
13
|
+
File.open(file, "r") {|f| content = f.readlines(chomp: true)}
|
14
|
+
|
15
|
+
count = 0
|
16
|
+
while count <= 999
|
17
|
+
begin
|
18
|
+
repo.create(stamp, content)
|
19
|
+
break # success to create a note
|
20
|
+
rescue Textrepo::DuplicateTimestampError => e
|
21
|
+
puts "A text with the timestamp [%s] has been already exists" \
|
22
|
+
" in the repository." % stamp
|
23
|
+
|
24
|
+
repo_text = repo.read(stamp)
|
25
|
+
if content == repo_text
|
26
|
+
# if the content is the same to the target file,
|
27
|
+
# the specified file has been already imported.
|
28
|
+
# Then, there is nothing to do. Just exit.
|
29
|
+
puts "The note [%s] in the repository exactly matches" \
|
30
|
+
" the specified file." % stamp
|
31
|
+
puts "It seems there is no need to import the file [%s]." % file
|
32
|
+
exit # normal end
|
33
|
+
else
|
34
|
+
puts "The text in the repository does not match the" \
|
35
|
+
" specified file."
|
36
|
+
count += 1
|
37
|
+
stamp = Textrepo::Timestamp.new(stamp.time, count)
|
38
|
+
puts "Try to create a note again with a new " \
|
39
|
+
"timestamp [%s]." % stamp
|
40
|
+
end
|
41
|
+
rescue Textrepo::EmptyTextError => e
|
42
|
+
puts "... aborted."
|
43
|
+
puts "The specified file is empyt."
|
44
|
+
exit 1 # error
|
45
|
+
end
|
46
|
+
end
|
47
|
+
if count > 999
|
48
|
+
puts "Cannot create a text into the repository with the" \
|
49
|
+
" specified file [%s]." % file
|
50
|
+
puts "For, the birthtime [%s] is identical to some notes" \
|
51
|
+
" already exists in the reopsitory." % btime
|
52
|
+
puts "Change the birthtime of the target file, then retry."
|
53
|
+
else
|
54
|
+
puts "... Done."
|
55
|
+
end
|
56
|
+
else
|
57
|
+
puts "not supecified FILE"
|
58
|
+
super
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "io/console/size"
|
2
|
+
|
3
|
+
module Rbnotes
|
4
|
+
class Commands::List < Commands::Command
|
5
|
+
def execute(args, conf)
|
6
|
+
@row, @column = IO.console_size
|
7
|
+
max = (args.shift || @row - 3).to_i
|
8
|
+
|
9
|
+
@repo = Textrepo.init(conf)
|
10
|
+
notes = @repo.entries.sort{|a, b| b <=> a}
|
11
|
+
notes[0, max].each { |timestamp_str|
|
12
|
+
puts make_headline(timestamp_str)
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
TIMESTAMP_STR_MAX_WIDTH = "yyyymoddhhmiss_sfx".size
|
18
|
+
# Makes a headline with the timestamp and subject of the notes, it
|
19
|
+
# looks like as follows:
|
20
|
+
#
|
21
|
+
# |<------------------ console column size --------------------->|
|
22
|
+
# +-- timestamp ---+ +--- subject (the 1st line of each note) --+
|
23
|
+
# | | | |
|
24
|
+
# 20101010001000_123: # I love Macintosh. [EOL]
|
25
|
+
# 20100909090909_999: # This is very very long long loooong subje[EOL]
|
26
|
+
# ++
|
27
|
+
# ^--- delimiter (2 characters)
|
28
|
+
#
|
29
|
+
# The subject part will truncate when it is long.
|
30
|
+
def make_headline(timestamp_str)
|
31
|
+
|
32
|
+
delimiter = ": "
|
33
|
+
subject_width = @column - TIMESTAMP_STR_MAX_WIDTH - delimiter.size - 1
|
34
|
+
|
35
|
+
subject = @repo.read(Textrepo::Timestamp.parse_s(timestamp_str))[0]
|
36
|
+
prefix = '# '
|
37
|
+
subject = prefix + subject.lstrip if subject[0, 2] != prefix
|
38
|
+
|
39
|
+
ts_part = "#{timestamp_str} "[0..(TIMESTAMP_STR_MAX_WIDTH - 1)]
|
40
|
+
sj_part = truncate_str(subject, subject_width)
|
41
|
+
|
42
|
+
ts_part + delimiter + sj_part
|
43
|
+
end
|
44
|
+
|
45
|
+
def truncate_str(str, size)
|
46
|
+
count = 0
|
47
|
+
result = ""
|
48
|
+
str.each_char { |c|
|
49
|
+
# TODO: fix
|
50
|
+
# This code is ugly. It assumes that each non-ascii character
|
51
|
+
# always occupy the width of 2 ascii characters in a terminal.
|
52
|
+
# I am not sure the assumption is appropriate or not.
|
53
|
+
count += c.ascii_only? ? 1 : 2
|
54
|
+
break if count > size
|
55
|
+
result << c
|
56
|
+
}
|
57
|
+
result
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Rbnotes
|
2
|
+
class Commands::Show < Commands::Command
|
3
|
+
def execute(args, conf)
|
4
|
+
stamp_str = args.shift
|
5
|
+
unless stamp_str.nil?
|
6
|
+
repo = Textrepo.init(conf)
|
7
|
+
stamp = Textrepo::Timestamp.parse_s(stamp_str)
|
8
|
+
content = repo.read(stamp)
|
9
|
+
|
10
|
+
pager = conf[:pager]
|
11
|
+
unless pager.nil?
|
12
|
+
require 'open3'
|
13
|
+
Open3.pipeline_w(pager) { |stdin|
|
14
|
+
stdin.puts content
|
15
|
+
stdin.close
|
16
|
+
}
|
17
|
+
else
|
18
|
+
puts content
|
19
|
+
end
|
20
|
+
else
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift File.expand_path("lib", __dir__)
|
5
|
+
|
6
|
+
require "rbnotes"
|
7
|
+
|
8
|
+
DEBUG = true
|
9
|
+
|
10
|
+
module Rbnotes
|
11
|
+
class App
|
12
|
+
def initialize
|
13
|
+
@conf = default_conf
|
14
|
+
end
|
15
|
+
|
16
|
+
def default_conf
|
17
|
+
{
|
18
|
+
:repository_type => :file_system,
|
19
|
+
:repository_name => DEBUG ? "sandbox/notes_dev" : "notes",
|
20
|
+
:repository_base => File.expand_path("~"),
|
21
|
+
:pager => "bat",
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def run(args)
|
26
|
+
cmd = args.shift
|
27
|
+
Rbnotes::Commands.load(cmd).execute(args, @conf)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
app = Rbnotes::App.new
|
33
|
+
app.run(ARGV)
|
data/lib/textrepo.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module Textrepo
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
module ErrMsg
|
5
|
+
UNKNOWN_REPO_TYPE = 'unknown type for repository: %s'
|
6
|
+
DUPLICATE_TIMESTAMP = 'duplicate timestamp: %s'
|
7
|
+
EMPTY_TEXT = 'empty text'
|
8
|
+
MISSING_TIMESTAMP = 'missing timestamp: %s'
|
9
|
+
end
|
10
|
+
|
11
|
+
class UnknownRepoTypeError < Error
|
12
|
+
def initialize(type)
|
13
|
+
super(ErrMsg::UNKNOWN_REPO_TYPE % type)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Following errors might occur in repository operations:
|
18
|
+
# +--------------------------+---------------------+
|
19
|
+
# | operation (args) | error type |
|
20
|
+
# +--------------------------+---------------------+
|
21
|
+
# | create (timestamp, text) | Duplicate timestamp |
|
22
|
+
# | | Empty text |
|
23
|
+
# +--------------------------+---------------------+
|
24
|
+
# | read (timestamp) | Missing timestamp |
|
25
|
+
# +--------------------------+---------------------+
|
26
|
+
# | update (timestamp, text) | Mssing timestamp |
|
27
|
+
# | | Empty text |
|
28
|
+
# +--------------------------+---------------------+
|
29
|
+
# | delete (timestamp) | Missing timestamp |
|
30
|
+
# +--------------------------+---------------------+
|
31
|
+
|
32
|
+
class DuplicateTimestampError < Error
|
33
|
+
def initialize(timestamp)
|
34
|
+
super(ErrMsg::DUPLICATE_TIMESTAMP % timestamp)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class EmptyTextError < Error
|
39
|
+
def initialize
|
40
|
+
super(ErrMsg::EMPTY_TEXT)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class MissingTimestampError < Error
|
45
|
+
def initialize(timestamp)
|
46
|
+
super(ErrMsg::MISSING_TIMESTAMP % timestamp)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Textrepo
|
4
|
+
# A concrete repository which uses the default file system as a storage.
|
5
|
+
class FileSystemRepository < Repository
|
6
|
+
attr_reader :path, :extname
|
7
|
+
|
8
|
+
FAVORITE_REPOSITORY_NAME = 'notes'
|
9
|
+
FAVORITE_EXTNAME = 'md'
|
10
|
+
|
11
|
+
# `conf` must be a Hash object. It must hold the follwoing
|
12
|
+
# values:
|
13
|
+
#
|
14
|
+
# - :repository_type (:file_system)
|
15
|
+
# - :repository_name => basename of the root path for the repository
|
16
|
+
# - :repository_base => the parent directory path for the repository
|
17
|
+
# - :default_extname => extname for a file stored into in the repository
|
18
|
+
#
|
19
|
+
# The root path of the repository looks like the following:
|
20
|
+
# - conf[:repository_base]/conf[:repository_name]
|
21
|
+
#
|
22
|
+
# Default values are set when `repository_name` and `default_extname`
|
23
|
+
# were not defined in `conf`.
|
24
|
+
def initialize(conf)
|
25
|
+
super
|
26
|
+
base = conf[:repository_base]
|
27
|
+
@name ||= FAVORITE_REPOSITORY_NAME
|
28
|
+
@path = File.expand_path("#{name}", base)
|
29
|
+
FileUtils.mkdir_p(@path)
|
30
|
+
@extname = conf[:default_extname] || FAVORITE_EXTNAME
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# repository operations
|
35
|
+
#
|
36
|
+
|
37
|
+
# Creates a file into the repository, which contains the specified
|
38
|
+
# text and is associated to the timestamp.
|
39
|
+
def create(timestamp, text)
|
40
|
+
abs = abspath(timestamp)
|
41
|
+
raise DuplicateTimestampError, timestamp if FileTest.exist?(abs)
|
42
|
+
raise EmptyTextError if text.nil? || text.empty?
|
43
|
+
|
44
|
+
write_text(abs, text)
|
45
|
+
timestamp
|
46
|
+
end
|
47
|
+
|
48
|
+
# Reads the file content in the repository. Then, returns its
|
49
|
+
# content.
|
50
|
+
def read(timestamp)
|
51
|
+
abs = abspath(timestamp)
|
52
|
+
raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
|
53
|
+
content = nil
|
54
|
+
File.open(abs, 'r') { |f|
|
55
|
+
content = f.readlines(chomp: true)
|
56
|
+
}
|
57
|
+
content
|
58
|
+
end
|
59
|
+
|
60
|
+
# Updates the file content in the repository. A new timestamp
|
61
|
+
# will be attached to the text.
|
62
|
+
def update(timestamp, text)
|
63
|
+
raise EmptyTextError if text.empty?
|
64
|
+
org_abs = abspath(timestamp)
|
65
|
+
raise MissingTimestampError, timestamp unless FileTest.exist?(org_abs)
|
66
|
+
|
67
|
+
# the text must be stored with the new timestamp
|
68
|
+
new_stamp = Timestamp.new(Time.now)
|
69
|
+
new_abs = abspath(new_stamp)
|
70
|
+
write_text(new_abs, text)
|
71
|
+
|
72
|
+
# delete the original file in the repository
|
73
|
+
FileUtils.remove_file(org_abs)
|
74
|
+
|
75
|
+
new_stamp
|
76
|
+
end
|
77
|
+
|
78
|
+
# Deletes the file in the repository.
|
79
|
+
def delete(timestamp)
|
80
|
+
abs = abspath(timestamp)
|
81
|
+
raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
|
82
|
+
content = read(timestamp)
|
83
|
+
|
84
|
+
FileUtils.remove_file(abs)
|
85
|
+
|
86
|
+
content
|
87
|
+
end
|
88
|
+
|
89
|
+
# Finds entries of text those timestamp matches the specified pattern.
|
90
|
+
def entries(stamp_pattern = nil)
|
91
|
+
results = []
|
92
|
+
|
93
|
+
case stamp_pattern.to_s.size
|
94
|
+
when "yyyymoddhhmiss_lll".size
|
95
|
+
stamp = Timestamp.parse_s(stamp_pattern)
|
96
|
+
if exist?(stamp)
|
97
|
+
results << stamp.to_s
|
98
|
+
end
|
99
|
+
when 0, "yyyymoddhhmiss".size, "yyyymodd".size
|
100
|
+
results += find_entries(stamp_pattern)
|
101
|
+
when 4 # "yyyy" or "modd"
|
102
|
+
pat = nil
|
103
|
+
# The following distinction is practically correct, but not
|
104
|
+
# perfect. It simply assumes that a year is greater than
|
105
|
+
# 1231. For, a year before 1232 is too old for us to create
|
106
|
+
# any text (I believe...).
|
107
|
+
if stamp_pattern.to_i > 1231
|
108
|
+
# yyyy
|
109
|
+
pat = stamp_pattern
|
110
|
+
else
|
111
|
+
# modd
|
112
|
+
pat = "*#{stamp_pattern}"
|
113
|
+
end
|
114
|
+
results += find_entries(pat)
|
115
|
+
end
|
116
|
+
|
117
|
+
results
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
def abspath(timestamp)
|
122
|
+
filename = timestamp.to_pathname + ".#{@extname}"
|
123
|
+
File.expand_path(filename, @path)
|
124
|
+
end
|
125
|
+
|
126
|
+
def write_text(abs, text)
|
127
|
+
FileUtils.mkdir_p(File.dirname(abs))
|
128
|
+
File.open(abs, 'w') { |f|
|
129
|
+
text.each {|line| f.puts(line) }
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
def timestamp_str(text_path)
|
134
|
+
File.basename(text_path).delete_suffix(".#{@extname}")
|
135
|
+
end
|
136
|
+
|
137
|
+
def exist?(timestamp)
|
138
|
+
FileTest.exist?(abspath(timestamp))
|
139
|
+
end
|
140
|
+
|
141
|
+
def find_entries(stamp_pattern)
|
142
|
+
Dir.glob("#{@path}/**/#{stamp_pattern}*.#{@extname}").map { |e|
|
143
|
+
timestamp_str(e)
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Textrepo
|
2
|
+
class Repository
|
3
|
+
attr_reader :type, :name
|
4
|
+
|
5
|
+
def initialize(conf)
|
6
|
+
@type = conf[:repository_type]
|
7
|
+
@name = conf[:repository_name]
|
8
|
+
end
|
9
|
+
|
10
|
+
# Stores text data into the repository with the specified timestamp.
|
11
|
+
# Returns the timestamp.
|
12
|
+
def create(timestamp, text); timestamp; end
|
13
|
+
|
14
|
+
# Reads text data from the repository, which is associated to the
|
15
|
+
# timestamp. Returns an array which contains the text.
|
16
|
+
def read(timestamp); []; end
|
17
|
+
|
18
|
+
# Updates the content with text in the repository, which is
|
19
|
+
# associated to the timestamp. Returns the timestamp.
|
20
|
+
def update(timestamp, text); timestamp; end
|
21
|
+
|
22
|
+
# Deletes the content in the repository, which is associated to
|
23
|
+
# the timestamp. Returns an array which contains the deleted text.
|
24
|
+
def delete(timestamp); []; end
|
25
|
+
|
26
|
+
# Finds all entries of text those have timestamps which mathes the
|
27
|
+
# specified pattern of timestamp. Returns an array which contains
|
28
|
+
# timestamps. A pattern must be one of the following:
|
29
|
+
#
|
30
|
+
# - yyyymoddhhmiss_lll : whole stamp
|
31
|
+
# - yyyymoddhhmiss : omit millisecond part
|
32
|
+
# - yyyymodd : date part only
|
33
|
+
# - yyyymo : month and year
|
34
|
+
# - yyyy : year only
|
35
|
+
# - modd : month and day
|
36
|
+
#
|
37
|
+
# If `stamp_pattern` is omitted, the recent entries will be listed.
|
38
|
+
# Then, how many entries are listed depends on the implementaiton
|
39
|
+
# of the concrete repository class.
|
40
|
+
def entries(stamp_pattern = nil); []; end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
require_relative 'file_system_repository'
|
45
|
+
|
46
|
+
# Returns an instance which derived from Textrepo::Repository class.
|
47
|
+
# `conf` must be a Hash object which has a value of
|
48
|
+
# `:repository_type` and `:repository_name` at least. Some concrete
|
49
|
+
# class derived from Textrepo::Repository may require more key-value
|
50
|
+
# pairs in `conf`.
|
51
|
+
def init(conf)
|
52
|
+
type = conf[:repository_type]
|
53
|
+
klass_name = type.to_s.split(/_/).map(&:capitalize).join + "Repository"
|
54
|
+
if Textrepo.const_defined?(klass_name)
|
55
|
+
klass = Textrepo.const_get(klass_name, false)
|
56
|
+
else
|
57
|
+
raise UnknownRepoTypeError, type.nil? ? "(nil)" : type
|
58
|
+
end
|
59
|
+
klass.new(conf)
|
60
|
+
end
|
61
|
+
module_function :init
|
62
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Textrepo
|
2
|
+
class Timestamp
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
attr_reader :time, :suffix
|
6
|
+
|
7
|
+
# time: a Time instance
|
8
|
+
# suffix: an Integer instance
|
9
|
+
def initialize(time, suffix = nil)
|
10
|
+
@time = time
|
11
|
+
@suffix = suffix
|
12
|
+
end
|
13
|
+
|
14
|
+
def <=>(other)
|
15
|
+
result = (self.time <=> other.time)
|
16
|
+
|
17
|
+
sfx = self.suffix || 0
|
18
|
+
osfx = other.suffix || 0
|
19
|
+
|
20
|
+
result == 0 ? (sfx <=> osfx) : result
|
21
|
+
end
|
22
|
+
|
23
|
+
# %Y %m %d %H %M %S suffix
|
24
|
+
# "2020-12-30 12:34:56 (0 | nil)" => "20201230123456"
|
25
|
+
# "2020-12-30 12:34:56 (7)" => "20201230123456_007"
|
26
|
+
def to_s
|
27
|
+
s = @time.strftime("%Y%m%d%H%M%S")
|
28
|
+
s += "_#{"%03u" % @suffix}" unless @suffix.nil? || @suffix == 0
|
29
|
+
s
|
30
|
+
end
|
31
|
+
|
32
|
+
# %Y %m %d %H %M %S suffix %Y/%m/ %Y%m%d%H%M%S %L
|
33
|
+
# "2020-12-30 12:34:56 (0 | nil)" => "2020/12/20201230123456"
|
34
|
+
# "2020-12-30 12:34:56 (7)" => "2020/12/20201230123456_007"
|
35
|
+
def to_pathname
|
36
|
+
@time.strftime("%Y/%m/") + self.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
class << self
|
40
|
+
# yyyymoddhhmiss sfx yyyy mo dd hh mi ss sfx
|
41
|
+
# "20201230123456" => "2020", "12", "30", "12", "34", "56"
|
42
|
+
# "20201230123456_789" => "2020", "12", "30", "12", "34", "56", "789"
|
43
|
+
def split_stamp(stamp_str)
|
44
|
+
# yyyy mo dd hh mi ss sfx
|
45
|
+
a = [0..3, 4..5, 6..7, 8..9, 10..11, 12..13, 15..17].map {|r| stamp_str[r]}
|
46
|
+
a[-1].nil? ? a[0..-2] : a
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_s(stamp_str)
|
50
|
+
year, mon, day, hour, min, sec , sfx = split_stamp(stamp_str).map(&:to_i)
|
51
|
+
Timestamp.new(Time.new(year, mon, day, hour, min, sec), sfx)
|
52
|
+
end
|
53
|
+
|
54
|
+
# (-2)
|
55
|
+
# 0 8 |(-1)
|
56
|
+
# V V VV
|
57
|
+
# "2020/12/20201230123456" => "2020-12-30 12:34:56"
|
58
|
+
def parse_pathname(pathname)
|
59
|
+
parse_s(pathname[8..-1])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/textrepo.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative 'lib/textrepo/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "textrepo"
|
5
|
+
spec.version = Textrepo::VERSION
|
6
|
+
spec.authors = ["mnbi"]
|
7
|
+
spec.email = ["mnbi@users.noreply.github.com"]
|
8
|
+
|
9
|
+
spec.summary = %q{A repository to store text with timestamp.}
|
10
|
+
spec.description = %q{Textrepo is a repository to store text with timestamp. It can manage text with the attached timestamp (read/update/delte).}
|
11
|
+
spec.homepage = "https://github.com/mnbi/textrepo"
|
12
|
+
spec.license = "MIT"
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
|
14
|
+
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/mnbi/textrepo"
|
17
|
+
spec.metadata["changelog_uri"] = "https://github.com/mnbi/textrepo/blob/main/CHANGELOG.md"
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
end
|
24
|
+
spec.bindir = "exe"
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 2.1"
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: textrepo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- mnbi
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-10-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.1'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.1'
|
27
|
+
description: Textrepo is a repository to store text with timestamp. It can manage
|
28
|
+
text with the attached timestamp (read/update/delte).
|
29
|
+
email:
|
30
|
+
- mnbi@users.noreply.github.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- ".gitignore"
|
36
|
+
- ".travis.yml"
|
37
|
+
- CHANGELOG.md
|
38
|
+
- Gemfile
|
39
|
+
- Gemfile.lock
|
40
|
+
- LICENSE.txt
|
41
|
+
- README.md
|
42
|
+
- Rakefile
|
43
|
+
- bin/console
|
44
|
+
- bin/setup
|
45
|
+
- examples/rbnotes/lib/rbnotes.rb
|
46
|
+
- examples/rbnotes/lib/rbnotes/commands.rb
|
47
|
+
- examples/rbnotes/lib/rbnotes/commands/import.rb
|
48
|
+
- examples/rbnotes/lib/rbnotes/commands/list.rb
|
49
|
+
- examples/rbnotes/lib/rbnotes/commands/show.rb
|
50
|
+
- examples/rbnotes/lib/rbnotes/version.rb
|
51
|
+
- examples/rbnotes/rbnotes
|
52
|
+
- lib/textrepo.rb
|
53
|
+
- lib/textrepo/error.rb
|
54
|
+
- lib/textrepo/file_system_repository.rb
|
55
|
+
- lib/textrepo/repository.rb
|
56
|
+
- lib/textrepo/timestamp.rb
|
57
|
+
- lib/textrepo/version.rb
|
58
|
+
- textrepo.gemspec
|
59
|
+
homepage: https://github.com/mnbi/textrepo
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
metadata:
|
63
|
+
homepage_uri: https://github.com/mnbi/textrepo
|
64
|
+
source_code_uri: https://github.com/mnbi/textrepo
|
65
|
+
changelog_uri: https://github.com/mnbi/textrepo/blob/main/CHANGELOG.md
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 2.7.0
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
requirements: []
|
81
|
+
rubygems_version: 3.1.4
|
82
|
+
signing_key:
|
83
|
+
specification_version: 4
|
84
|
+
summary: A repository to store text with timestamp.
|
85
|
+
test_files: []
|