File Coverage

blib/lib/Data/SimpleKV.pm
Criterion Covered Total %
statement 27 110 24.5
branch 0 60 0.0
condition 0 13 0.0
subroutine 9 20 45.0
pod 7 8 87.5
total 43 211 20.3


line stmt bran cond sub pod time code
1             package Data::SimpleKV;
2              
3 1     1   104255 use strict;
  1         3  
  1         47  
4 1     1   6 use warnings;
  1         2  
  1         58  
5 1     1   648 use Storable qw(store_fd retrieve_fd);
  1         3640  
  1         63  
6 1     1   6 use Fcntl qw(:flock SEEK_SET);
  1         2  
  1         163  
7 1     1   6 use File::Path qw(make_path);
  1         1  
  1         59  
8 1     1   3 use File::Spec;
  1         1  
  1         23  
9 1     1   489 use Encode qw(encode_utf8 decode_utf8);
  1         10821  
  1         97  
10 1     1   7 use Carp;
  1         2  
  1         48  
11 1     1   457 use utf8;
  1         215  
  1         4  
12              
13             our $VERSION = '0.03';
14              
15             =encoding utf8
16              
17             =head1 NAME
18              
19             Data::SimpleKV - A simple key-value database with memory cache and disk persistence
20              
21             =head1 SYNOPSIS
22              
23             use Data::SimpleKV;
24            
25             my $db = Data::SimpleKV->new(
26             db_name => 'myapp',
27             data_dir => '/var/lib/simplekv' # optional
28             );
29            
30             $db->set('key1', '测试值');
31             my $value = $db->get('key1');
32             my $exists = $db->exists('key1');
33             $db->delete('key1');
34             $db->save();
35              
36             =head1 DESCRIPTION
37              
38             Data::SimpleKV provides a simple key-value database with in-memory caching and
39             disk persistence. It supports UTF-8 data storage and is safe for multi-process usage.
40              
41             =cut
42              
43             sub new {
44 0     0 0   my ($class, %args) = @_;
45            
46 0   0       my $db_name = $args{db_name} || croak "db_name is required";
47            
48             # 确定数据目录
49 0           my $data_dir = $args{data_dir};
50 0 0         if (!$data_dir) {
51             # 尝试使用 /var/lib/simplekv
52 0           my $system_dir = '/var/lib/simplekv';
53 0 0         if (-w '/var/lib') {
54             # 有权限访问 /var/lib,尝试创建或使用 simplekv 目录
55 0 0 0       if (-d $system_dir || mkdir($system_dir, 0755)) {
56 0           $data_dir = $system_dir;
57             }
58             }
59              
60             # 如果系统目录不可用,使用用户目录
61 0 0         if (!$data_dir) {
62 0   0       $data_dir = File::Spec->catdir($ENV{HOME} || '/tmp', '.simplekv');
63             }
64             }
65              
66             # 创建数据目录(如果不存在)
67 0 0         make_path($data_dir, { mode => 0755 }) unless -d $data_dir;
68              
69            
70             # 数据文件路径
71 0           my $data_file = File::Spec->catfile($data_dir, "${db_name}.db");
72            
73 0           my $self = {
74             db_name => $db_name,
75             data_dir => $data_dir,
76             data_file => $data_file,
77             cache => {},
78             dirty => 0, # 标记是否有未保存的更改
79             };
80            
81 0           bless $self, $class;
82            
83             # 加载现有数据
84 0           $self->_load_data();
85            
86 0           return $self;
87             }
88              
89             =head2 get($key)
90              
91             Get value by key. Returns undef if key doesn't exist.
92              
93             =cut
94              
95             sub get {
96 0     0 1   my ($self, $key) = @_;
97 0 0         croak "Key is required" unless defined $key;
98            
99             # UTF-8编码处理
100 0 0         $key = encode_utf8($key) if utf8::is_utf8($key);
101            
102 0           my $value = $self->{cache}{$key};
103            
104             # 如果值是UTF-8字节串,解码为字符串
105 0 0 0       if (defined $value && !utf8::is_utf8($value)) {
106 0           $value = decode_utf8($value, Encode::FB_QUIET);
107             }
108            
109 0           return $value;
110             }
111              
112             =head2 set($key, $value)
113              
114             Set key-value pair.
115              
116             =cut
117              
118             sub set {
119 0     0 1   my ($self, $key, $value) = @_;
120 0 0         croak "Key is required" unless defined $key;
121 0 0         croak "Value is required" unless defined $value;
122            
123             # UTF-8编码处理
124 0 0         $key = encode_utf8($key) if utf8::is_utf8($key);
125 0 0         $value = encode_utf8($value) if utf8::is_utf8($value);
126            
127 0           $self->{cache}{$key} = $value;
128 0           $self->{dirty} = 1;
129            
130 0           return 1;
131             }
132              
133             =head2 delete($key)
134              
135             Delete key-value pair. Returns 1 if key existed, 0 otherwise.
136              
137             =cut
138              
139             sub delete {
140 0     0 1   my ($self, $key) = @_;
141 0 0         croak "Key is required" unless defined $key;
142              
143             # UTF-8编码处理
144 0 0         $key = encode_utf8($key) if utf8::is_utf8($key);
145              
146 0 0         if (exists $self->{cache}{$key}) {
147 0           delete $self->{cache}{$key};
148 0           $self->{dirty} = 1;
149 0           return 1;
150             }
151              
152 0           return 0;
153             }
154              
155             =head2 exists($key)
156              
157             Check if key exists. Returns 1 if exists, 0 otherwise.
158              
159             =cut
160              
161             sub exists {
162 0     0 1   my ($self, $key) = @_;
163 0 0         croak "Key is required" unless defined $key;
164            
165             # UTF-8编码处理
166 0 0         $key = encode_utf8($key) if utf8::is_utf8($key);
167            
168 0 0         return exists $self->{cache}{$key} ? 1 : 0;
169             }
170              
171             =head2 save()
172              
173             Save data to disk. This method is process-safe using file locking.
174              
175             =cut
176              
177             sub save {
178 0     0 1   my ($self) = @_;
179            
180             # 如果没有更改,不需要保存
181 0 0         return 1 unless $self->{dirty};
182            
183 0           return $self->_save_with_lock();
184             }
185              
186             =head2 keys()
187              
188             Get all keys as a list.
189              
190             =cut
191              
192             sub keys {
193 0     0 1   my ($self) = @_;
194              
195             return map {
196 0 0         eval { decode_utf8($_) } || $_
  0            
197 0           } keys %{$self->{cache}};
  0            
198             }
199              
200             =head2 clear()
201              
202             Clear all data from memory cache.
203              
204             =cut
205              
206             sub clear {
207 0     0 1   my ($self) = @_;
208 0           $self->{cache} = {};
209 0           $self->{dirty} = 1;
210 0           return 1;
211             }
212              
213             # 私有方法:加载数据
214             sub _load_data {
215 0     0     my ($self) = @_;
216            
217 0 0         return unless -f $self->{data_file};
218            
219 0           eval {
220 0 0         open my $fh, '<', $self->{data_file} or die "Cannot open data file: $!";
221            
222 0           my $data = retrieve_fd($fh);
223 0   0       $self->{cache} = $data || {};
224            
225 0           close $fh;
226             };
227            
228 0 0         if ($@) {
229 0           warn "Failed to load data: $@";
230 0           $self->{cache} = {};
231             }
232             }
233              
234             # 私有方法:带锁保存数据
235             sub _save_with_lock {
236 0     0     my ($self) = @_;
237            
238 0           eval {
239             # 使用 +> 模式:如果文件不存在则创建,如果存在则截断
240 0 0         open my $fh, '+>', $self->{data_file} or die "Cannot open data file: $!";
241            
242             # 获取排他锁
243 0 0         flock($fh, LOCK_EX) or die "Cannot acquire exclusive lock: $!";
244            
245             # 确保从文件开头写入
246 0           seek($fh, 0, SEEK_SET);
247            
248             # 存储数据
249 0 0         store_fd($self->{cache}, $fh) or die "Cannot store data: $!";
250            
251             # 确保数据写入磁盘
252 0           $fh->flush();
253            
254 0 0         close $fh or die "Cannot close data file: $!";
255            
256 0           chmod 0644, $self->{data_file};
257 0           $self->{dirty} = 0;
258             };
259            
260 0 0         if ($@) {
261 0           croak "Failed to save data: $@";
262             }
263            
264 0           return 1;
265             }
266              
267             # 析构函数:自动保存未保存的更改
268             sub DESTROY {
269 0     0     my ($self) = @_;
270            
271 0 0         if ($self->{dirty}) {
272 0           eval { $self->save() };
  0            
273 0 0         warn "Auto-save failed during destruction: $@" if $@;
274             }
275             }
276              
277             1;
278              
279             __END__