timet 1.6.2 → 1.6.4
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.
- checksums.yaml +4 -4
- data/.reek.yml +17 -1
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +35 -2
- data/README.md +1 -0
- data/lib/timet/application.rb +3 -3
- data/lib/timet/database.rb +25 -25
- data/lib/timet/database_syncer.rb +133 -140
- data/lib/timet/s3_supabase.rb +74 -131
- data/lib/timet/table.rb +0 -4
- data/lib/timet/tag_distribution.rb +181 -160
- data/lib/timet/time_report.rb +17 -3
- data/lib/timet/validation_editor.rb +303 -0
- data/lib/timet/version.rb +2 -2
- metadata +3 -7
- data/lib/timet/item_data_helper.rb +0 -59
- data/lib/timet/time_report_helper.rb +0 -36
- data/lib/timet/time_update_helper.rb +0 -75
- data/lib/timet/time_validation_helper.rb +0 -175
- data/lib/timet/validation_edit_helper.rb +0 -160
data/lib/timet/s3_supabase.rb
CHANGED
|
@@ -5,60 +5,36 @@ require 'logger'
|
|
|
5
5
|
require 'dotenv'
|
|
6
6
|
require 'fileutils'
|
|
7
7
|
|
|
8
|
-
#
|
|
9
|
-
# - S3 integration for data backup and sync
|
|
10
|
-
#
|
|
8
|
+
# Top-level module for the Timet time tracking gem.
|
|
11
9
|
module Timet
|
|
12
|
-
# Required environment variables for S3 configuration
|
|
10
|
+
# Required environment variables for S3 configuration.
|
|
13
11
|
REQUIRED_ENV_VARS = %w[S3_ENDPOINT S3_ACCESS_KEY S3_SECRET_KEY].freeze
|
|
14
12
|
|
|
15
|
-
# Ensures that the environment file exists and contains the required variables.
|
|
16
|
-
# If the file doesn't exist, it creates it. If required variables are missing,
|
|
17
|
-
# it adds them with empty values.
|
|
18
|
-
#
|
|
19
|
-
# @param env_file_path [String] The path to the environment file
|
|
20
|
-
# @return [void]
|
|
21
|
-
# @example
|
|
22
|
-
# Timet.ensure_env_file_exists('/path/to/.env')
|
|
23
13
|
def self.ensure_env_file_exists(env_file_path)
|
|
14
|
+
ensure_directory_exists(env_file_path)
|
|
15
|
+
ensure_file_exists(env_file_path)
|
|
16
|
+
append_missing_env_vars(env_file_path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.ensure_directory_exists(env_file_path)
|
|
24
20
|
dir_path = File.dirname(env_file_path)
|
|
25
21
|
FileUtils.mkdir_p(dir_path)
|
|
22
|
+
end
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
def self.ensure_file_exists(env_file_path)
|
|
28
25
|
File.write(env_file_path, '', mode: 'a')
|
|
26
|
+
end
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
def self.append_missing_env_vars(env_file_path)
|
|
31
29
|
Dotenv.load(env_file_path)
|
|
32
30
|
missing_vars = REQUIRED_ENV_VARS.reject { |var| ENV.fetch(var, nil) }
|
|
33
|
-
|
|
34
|
-
# Append missing variables with empty values
|
|
35
31
|
return if missing_vars.empty?
|
|
36
32
|
|
|
37
33
|
File.write(env_file_path, "#{missing_vars.map { |var| "#{var}=''" }.join("\n")}\n", mode: 'a')
|
|
38
34
|
end
|
|
39
35
|
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
# listing objects, uploading and downloading files, deleting objects, and
|
|
43
|
-
# deleting a bucket.
|
|
44
|
-
#
|
|
45
|
-
# This class requires the following environment variables to be set:
|
|
46
|
-
# - S3_ENDPOINT: The endpoint URL for the S3-compatible storage service.
|
|
47
|
-
# - S3_ACCESS_KEY: The access key ID for authentication.
|
|
48
|
-
# - S3_SECRET_KEY: The secret access key for authentication.
|
|
49
|
-
# - S3_REGION: The region for the S3-compatible storage service (default: 'us-west-1').
|
|
50
|
-
#
|
|
51
|
-
# @example Basic usage
|
|
52
|
-
# s3_supabase = S3Supabase.new
|
|
53
|
-
# s3_supabase.create_bucket('my-bucket')
|
|
54
|
-
# s3_supabase.upload_file('my-bucket', '/path/to/local/file.txt', 'file.txt')
|
|
55
|
-
#
|
|
56
|
-
# @example Advanced operations
|
|
57
|
-
# s3_supabase.list_objects('my-bucket')
|
|
58
|
-
# s3_supabase.download_file('my-bucket', 'file.txt', '/path/to/download/file.txt')
|
|
59
|
-
# s3_supabase.delete_object('my-bucket', 'file.txt')
|
|
60
|
-
# s3_supabase.delete_bucket('my-bucket')
|
|
61
|
-
class S3Supabase
|
|
36
|
+
# Configuration constants for S3 Supabase integration.
|
|
37
|
+
module S3Config
|
|
62
38
|
ENV_FILE_PATH = File.join(Dir.home, '.timet', '.env')
|
|
63
39
|
Timet.ensure_env_file_exists(ENV_FILE_PATH)
|
|
64
40
|
Dotenv.load(ENV_FILE_PATH)
|
|
@@ -68,11 +44,15 @@ module Timet
|
|
|
68
44
|
S3_SECRET_KEY = ENV.fetch('S3_SECRET_KEY', nil)
|
|
69
45
|
S3_REGION = ENV.fetch('S3_REGION', 'us-west-1')
|
|
70
46
|
LOG_FILE_PATH = File.join(Dir.home, '.timet', 's3_supabase.log')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Struct to hold S3 object reference (bucket name and object key).
|
|
50
|
+
S3ObjectRef = Struct.new(:bucket_name, :object_key, keyword_init: true)
|
|
51
|
+
|
|
52
|
+
# S3Supabase provides methods to interact with an S3-compatible storage service.
|
|
53
|
+
class S3Supabase
|
|
54
|
+
include S3Config
|
|
71
55
|
|
|
72
|
-
# Initializes a new instance of the S3Supabase class.
|
|
73
|
-
# Sets up the AWS S3 client with the configured credentials and endpoint.
|
|
74
|
-
#
|
|
75
|
-
# @raise [CustomError] If required environment variables are missing
|
|
76
56
|
def initialize
|
|
77
57
|
validate_env_vars
|
|
78
58
|
@logger = Logger.new(LOG_FILE_PATH)
|
|
@@ -86,127 +66,92 @@ module Timet
|
|
|
86
66
|
)
|
|
87
67
|
end
|
|
88
68
|
|
|
89
|
-
# Creates a new bucket in the S3-compatible storage service.
|
|
90
|
-
#
|
|
91
|
-
# @param bucket_name [String] The name of the bucket to create
|
|
92
|
-
# @return [Boolean] true if bucket was created successfully, false otherwise
|
|
93
|
-
# @example
|
|
94
|
-
# create_bucket('my-new-bucket')
|
|
95
69
|
def create_bucket(bucket_name)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
70
|
+
@s3_client.create_bucket(bucket: bucket_name)
|
|
71
|
+
log(:info, "Bucket '#{bucket_name}' created successfully!")
|
|
72
|
+
true
|
|
73
|
+
rescue Aws::S3::Errors::BucketAlreadyExists
|
|
74
|
+
log(:error, "Error: The bucket '#{bucket_name}' already exists.")
|
|
75
|
+
false
|
|
76
|
+
rescue Aws::S3::Errors::BucketAlreadyOwnedByYou
|
|
77
|
+
log(:error, "Error: The bucket '#{bucket_name}' is already owned by you.")
|
|
78
|
+
false
|
|
79
|
+
rescue Aws::S3::Errors::ServiceError => e
|
|
80
|
+
log(:error, "Error creating bucket: #{e.message}")
|
|
107
81
|
false
|
|
108
82
|
end
|
|
109
83
|
|
|
110
|
-
# Lists all objects in the specified bucket.
|
|
111
|
-
#
|
|
112
|
-
# @param bucket_name [String] The name of the bucket to list objects from
|
|
113
|
-
# @return [Array<Hash>, false, nil] Array of object hashes if objects found, false if bucket is empty,
|
|
114
|
-
# nil if error occurs
|
|
115
|
-
# @raise [Aws::S3::Errors::ServiceError] if there's an error accessing the S3 service
|
|
116
|
-
# @example
|
|
117
|
-
# list_objects('my-bucket') #=> [{key: 'example.txt', last_modified: '2023-01-01', ...}, ...]
|
|
118
|
-
# list_objects('empty-bucket') #=> false
|
|
119
|
-
# list_objects('invalid-bucket') #=> nil
|
|
120
84
|
def list_objects(bucket_name)
|
|
121
85
|
response = @s3_client.list_objects_v2(bucket: bucket_name)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
response.contents.each { |object| @logger.info "- #{object.key} (Last modified: #{object.last_modified})" }
|
|
128
|
-
response.contents.map(&:to_h)
|
|
86
|
+
contents = response.contents
|
|
87
|
+
|
|
88
|
+
if contents.empty?
|
|
89
|
+
log(:error, "No objects found in '#{bucket_name}'.")
|
|
90
|
+
return false
|
|
129
91
|
end
|
|
92
|
+
|
|
93
|
+
log_object_list(contents, bucket_name)
|
|
94
|
+
contents.map(&:to_h)
|
|
130
95
|
rescue Aws::S3::Errors::ServiceError => e
|
|
131
|
-
|
|
96
|
+
log(:error, "Error listing objects: #{e.message}")
|
|
132
97
|
nil
|
|
133
98
|
end
|
|
134
99
|
|
|
135
|
-
# Uploads a file to the specified bucket.
|
|
136
|
-
#
|
|
137
|
-
# @param bucket_name [String] The name of the bucket to upload to
|
|
138
|
-
# @param file_path [String] The local path of the file to upload
|
|
139
|
-
# @param object_key [String] The key (name) to give the object in the bucket
|
|
140
|
-
# @return [void]
|
|
141
|
-
# @example
|
|
142
|
-
# upload_file('my-bucket', '/path/to/local/file.txt', 'remote-file.txt')
|
|
143
100
|
def upload_file(bucket_name, file_path, object_key)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
101
|
+
File.open(file_path, 'rb') do |file|
|
|
102
|
+
@s3_client.put_object(
|
|
103
|
+
bucket: bucket_name,
|
|
104
|
+
key: object_key,
|
|
105
|
+
body: file
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
log(:info, "File '#{object_key}' uploaded successfully.")
|
|
150
109
|
rescue Aws::S3::Errors::ServiceError => e
|
|
151
|
-
|
|
110
|
+
log(:error, "Error uploading file: #{e.message}")
|
|
152
111
|
end
|
|
153
112
|
|
|
154
|
-
# Downloads a file from the specified bucket.
|
|
155
|
-
#
|
|
156
|
-
# @param bucket_name [String] The name of the bucket to download from
|
|
157
|
-
# @param object_key [String] The key of the object to download
|
|
158
|
-
# @param download_path [String] The local path where the file should be saved
|
|
159
|
-
# @return [void]
|
|
160
|
-
# @example
|
|
161
|
-
# download_file('my-bucket', 'remote-file.txt', '/path/to/local/file.txt')
|
|
162
113
|
def download_file(bucket_name, object_key, download_path)
|
|
163
114
|
response = @s3_client.get_object(bucket: bucket_name, key: object_key)
|
|
164
115
|
File.binwrite(download_path, response.body.read)
|
|
165
|
-
|
|
116
|
+
log(:info, "File '#{object_key}' downloaded successfully.")
|
|
166
117
|
rescue Aws::S3::Errors::ServiceError => e
|
|
167
|
-
|
|
118
|
+
log(:error, "Error downloading file: #{e.message}")
|
|
168
119
|
end
|
|
169
120
|
|
|
170
|
-
# Deletes an object from the specified bucket.
|
|
171
|
-
#
|
|
172
|
-
# @param bucket_name [String] The name of the bucket containing the object
|
|
173
|
-
# @param object_key [String] The key of the object to delete
|
|
174
|
-
# @return [void]
|
|
175
|
-
# @example
|
|
176
|
-
# delete_object('my-bucket', 'file-to-delete.txt')
|
|
177
121
|
def delete_object(bucket_name, object_key)
|
|
178
122
|
@s3_client.delete_object(bucket: bucket_name, key: object_key)
|
|
179
|
-
|
|
123
|
+
log(:info, "Object '#{object_key}' deleted successfully.")
|
|
180
124
|
rescue Aws::S3::Errors::ServiceError => e
|
|
181
|
-
|
|
125
|
+
log(:error, "Error deleting object: #{e.message}")
|
|
182
126
|
raise e
|
|
183
127
|
end
|
|
184
128
|
|
|
185
|
-
# Deletes a bucket and all its contents.
|
|
186
|
-
# First deletes all objects in the bucket, then deletes the bucket itself.
|
|
187
|
-
#
|
|
188
|
-
# @param bucket_name [String] The name of the bucket to delete
|
|
189
|
-
# @return [void]
|
|
190
|
-
# @example
|
|
191
|
-
# delete_bucket('bucket-to-delete')
|
|
192
129
|
def delete_bucket(bucket_name)
|
|
193
|
-
|
|
194
|
-
@s3_client.list_objects_v2(bucket: bucket_name).contents.each do |object|
|
|
195
|
-
delete_object(bucket_name, object.key)
|
|
196
|
-
end
|
|
130
|
+
delete_all_objects_in_bucket(bucket_name)
|
|
197
131
|
@s3_client.delete_bucket(bucket: bucket_name)
|
|
198
|
-
|
|
132
|
+
log(:info, "Bucket '#{bucket_name}' deleted successfully.")
|
|
199
133
|
rescue Aws::S3::Errors::ServiceError => e
|
|
200
|
-
|
|
134
|
+
log(:error, "Error deleting bucket: #{e.message}")
|
|
201
135
|
raise e
|
|
202
136
|
end
|
|
203
137
|
|
|
204
138
|
private
|
|
205
139
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
140
|
+
def log(level, message)
|
|
141
|
+
@logger.send(level, message)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def log_object_list(contents, bucket_name)
|
|
145
|
+
log(:error, "Objects in '#{bucket_name}':")
|
|
146
|
+
contents.each { |object| log(:error, "- #{object.key} (Last modified: #{object.last_modified})") }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def delete_all_objects_in_bucket(bucket_name)
|
|
150
|
+
list_objects(bucket_name)
|
|
151
|
+
contents = @s3_client.list_objects_v2(bucket: bucket_name).contents
|
|
152
|
+
contents.each { |object| delete_object(bucket_name, object.key) }
|
|
153
|
+
end
|
|
154
|
+
|
|
210
155
|
def validate_env_vars
|
|
211
156
|
missing_vars = []
|
|
212
157
|
missing_vars.concat(check_env_var('S3_ENDPOINT', S3_ENDPOINT))
|
|
@@ -221,13 +166,11 @@ module Timet
|
|
|
221
166
|
def check_env_var(name, value)
|
|
222
167
|
return [] if value && !value.empty?
|
|
223
168
|
|
|
169
|
+
@s3_client&.list_objects_v2(bucket: 'dummy')
|
|
224
170
|
[name]
|
|
225
171
|
end
|
|
226
172
|
|
|
227
173
|
# Custom error class that suppresses the backtrace for cleaner error messages.
|
|
228
|
-
#
|
|
229
|
-
# @example
|
|
230
|
-
# raise CustomError, "Missing required environment variables"
|
|
231
174
|
class CustomError < StandardError
|
|
232
175
|
def backtrace
|
|
233
176
|
nil
|
data/lib/timet/table.rb
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# require_relative 'color_codes'
|
|
4
|
-
require 'timet/time_report_helper'
|
|
5
3
|
require_relative 'utils'
|
|
6
4
|
module Timet
|
|
7
5
|
# This class is responsible for formatting the output of the `timet` application.
|
|
8
6
|
# It provides methods for formatting the table header, separators, and rows.
|
|
9
7
|
class Table
|
|
10
|
-
include TimeReportHelper
|
|
11
|
-
|
|
12
8
|
attr_reader :filter, :items
|
|
13
9
|
|
|
14
10
|
def initialize(filter, items, db)
|