File Coverage

blib/lib/File/Create/Layout.pm
Criterion Covered Total %
statement 129 147 87.7
branch 74 104 71.1
condition 3 8 37.5
subroutine 8 10 80.0
pod 3 3 100.0
total 217 272 79.7


line stmt bran cond sub pod time code
1             package File::Create::Layout;
2              
3             our $DATE = '2019-04-16'; # DATE
4             our $VERSION = '0.060'; # VERSION
5              
6 2     2   55467 use 5.010001;
  2         17  
7 2     2   8 use strict;
  2         3  
  2         33  
8 2     2   7 use warnings;
  2         2  
  2         65  
9 2     2   2680 use Log::ger;
  2         138  
  2         9  
10              
11 2     2   1080 use File::chdir;
  2         5159  
  2         3611  
12              
13             require Exporter;
14             our @ISA = qw(Exporter);
15             our @EXPORT_OK = qw(
16             create_files_using_layout
17             );
18              
19             our %SPEC;
20              
21             my %arg_layout = (
22             layout => {
23             summary => 'Layout',
24             description => <<'_',
25              
26             See the module documentation for the format/specification of layout.
27              
28             _
29             schema => 'str*',
30             req => 1,
31             pos => 0,
32             },
33             );
34              
35             sub _decode_json {
36 7     7   10 state $json = do {
37 1         6 require JSON::MaybeXS;
38 1         7 JSON::MaybeXS->new->allow_nonref;
39             };
40 7         47 $json->decode(shift);
41             }
42              
43             sub _parse_layout {
44 30     30   11955 my $layout = shift;
45              
46 30         40 my @indents;
47             my @res;
48 30         37 my $linum = 0;
49 30         43 my $prev_is_dir = 0;
50 30         78 for my $line (split /^/, $layout) {
51 73         92 chomp $line;
52 73         89 my $orig_line = $line;
53              
54 73         86 $linum++;
55 73 100       212 next if $line =~ /\A\s*\z/;
56 70 100       132 next if $line =~ /\A\s*#/;
57              
58 68         172 $line =~ s/\A(\s*)//;
59 68         113 my $cur_indent = $1;
60 68 50       119 die "(layout):$linum: Tabs are not allowed: $orig_line"
61             if $cur_indent =~ /\t/;
62              
63 68         78 $cur_indent = length($cur_indent);
64              
65 68 100       100 if (!@indents) {
66 28         39 push @indents, $cur_indent;
67             }
68              
69 68 100       144 if ($cur_indent > $indents[-1]) {
    100          
70             # indent is deeper than previous spec-line, we require that the
71             # previous spec-line is directory
72 13 100       31 die "(layout):$linum: More indented than previous spec-line, but ".
73             "previous spec-line is not a directory: $orig_line"
74             unless $prev_is_dir;
75 12         15 push @indents, $cur_indent;
76             } elsif ($cur_indent < $indents[-1]) {
77             # indent is shallower than previous spec-line, find previous level
78 8         9 my $found = 0;
79 8         16 for my $i (reverse 0..$#indents) {
80 17 100       28 if ($cur_indent == $indents[$i]) {
81 6         7 $found++;
82 6         11 splice @indents, $i+1;
83 6         8 last;
84             }
85             }
86 8 100       32 die "(layout):$linum: Invalid indent, must return to one of ".
87             "previous levels' indent: $orig_line" unless $found;
88             }
89              
90             # parse filename
91 65         97 my $name;
92 65 100       94 if ($line =~ /\A"/) {
93 5 100       35 $line =~ s#\A(".*?(?
94             or die "(layout):$linum: Invalid quoted filename: $orig_line";
95 3         7 $name = $1;
96 3         5 eval { $name = _decode_json($name) };
  3         5  
97 3 50       7 die "(layout):$linum: Invalid JSON string in filename: $@: $1"
98             if $@;
99             } else {
100 60         137 $line =~ s!\A([^\s\(/]*)!!;
101 60         101 $name = $1;
102             }
103 63 100       100 die "(layout):$linum: Filename cannot be empty: $orig_line"
104             unless length($name);
105 62 100       116 die "(layout):$linum: Filename cannot contain slashes: $orig_line"
106             if $name =~ m!/!;
107 61 100       105 die "(layout):$linum: Filename cannot be . or ..: $orig_line"
108             if $name =~ m!\A\.\.?\z!;
109              
110 59         64 my $is_dir;
111 59 100       94 if ($line =~ s!\A/!!) {
112 15         19 $is_dir = 1;
113             } else {
114 44         48 $is_dir = 0;
115             }
116              
117 59         67 my ($orig_perm, $perm, $user, $group);
118 59 100       96 if ($line =~ /\A\(/) {
119 6 100       51 $line =~ s/\A\((?:([^,]*),([^,]*),)?([0-7]{3,4})\)//
120             or die "(layout):$linum: Invalid syntax in permission/owner: $orig_line";
121 2         4 $user = $1;
122 2         4 $group = $2;
123 2         4 $orig_perm = $3;
124 2         3 $perm = oct($3);
125             }
126              
127 55         60 my $sym_target;
128 55 100       85 if ($line =~ s/\s+->\s*//) {
129 5 100       20 die "(layout):$linum: Symlink cannot be a directory: $orig_line"
130             if $is_dir;
131             # parse symlink target
132 4 100       11 if ($line =~ /\A"/) {
133 3 100       20 $line =~ s#\A(".*?(?
134             or die "(layout):$linum: Invalid quoted symlink target: $orig_line";
135 2         5 $sym_target = $1;
136 2         3 eval { $sym_target = _decode_json($sym_target) };
  2         5  
137 2 50       5 die "(layout):$linum: Invalid JSON string in symlink target: $@: $1"
138             if $@;
139             } else {
140 1         3 $line =~ s!\A([^\s]*)!!;
141 1         3 $sym_target = $1;
142             }
143 3 100       14 die "(layout):$linum: Symlink target cannot be empty: $orig_line"
144             unless length($sym_target);
145             }
146              
147 52         55 my $extras;
148 52 100       79 if ($line =~ s/\s+(\S.*)//) {
149 2         4 $extras = $1;
150 2         3 eval { $extras = _decode_json("{$extras}") };
  2         6  
151 2 50       6 die "(layout):$linum: Invalid unquoted JSON hash in extras: $@: $extras"
152             if $@;
153 2 50       7 if (defined $extras->{content}) {
154 2 100       13 die "(layout):$linum: Directory must not have 'content': $@: $orig_line"
155             if $is_dir;
156             }
157             }
158              
159             push @res, {
160             name => $name,
161             is_dir => $is_dir,
162             is_symlink => defined($sym_target) ? 1:0,
163             (symlink_target => $sym_target) x !!(defined $sym_target),
164             level => $#indents >= 0 ? $#indents : 0,
165             _linum => $linum,
166             perm => $perm,
167             perm_octal => $orig_perm,
168             user => $user,
169             group => $group,
170 51 100       260 (content => $extras->{content}) x !!(defined $extras->{content}),
    50          
171             };
172              
173 51         121 $prev_is_dir = $is_dir;
174             }
175              
176 13         44 \@res;
177             }
178              
179             $SPEC{create_files_using_layout} = {
180             v => 1.1,
181             summary => 'Create files/directories according to a layout',
182             description => <<'_',
183              
184             This routine can be used to quickly create several files/directories according
185             to a layout which you specify. The layout uses a few simple rules and common
186             conventions usually found in Linux/Unix environment.
187              
188             You can use this routine e.g. in a test script.
189              
190             _
191             args => {
192             %arg_layout,
193             prefix => {
194             summary => 'Root directory to create the files/directories in',
195             description => <<'_',
196              
197             Directory must already exist.
198              
199             If unspecified, will simply create starting from current directory.
200              
201             _
202             schema => 'str*',
203             },
204             },
205             };
206             sub create_files_using_layout {
207 1     1 1 1390 require File::chown;
208              
209 1         419 my %args = @_;
210              
211 1         3 my $parse_res;
212 1         1 eval { $parse_res = _parse_layout($args{layout}) };
  1         4  
213 1 50       4 return [400, "Syntax error in layout: $@"] if $@;
214              
215 1         3 my $prefix = $args{prefix};
216 1   33     8 local $CWD = $prefix // $CWD;
217 1   50     55 $prefix //= ".";
218              
219 1         2 my $prev_level;
220             my @dirs;
221 1         3 for my $e (@$parse_res) {
222 33         63 my $p = $prefix . join("", map {"/$_"} @dirs);
  51         151  
223              
224 33 100       57 if (defined $prev_level) {
225 32 100       67 if ($e->{level} > $prev_level) {
    100          
226 8         25 log_trace("chdir %s ...", $dirs[-1]);
227 8         18 eval { $CWD = $dirs[-1] };
  8         25  
228 8 50       154 return [500, "Can't chdir to $p/$e->{name}: $! (cwd=$CWD)"] if $@;
229             } elsif ($e->{level} < $prev_level) {
230 5         11 my $dir = join("/", (("..") x ($prev_level - $e->{level})));
231 5         10 splice @dirs, $e->{level};
232 5         8 $p = $prefix . join("", map {"/$_"} @dirs);
  3         7  
233 5         17 log_trace("chdir back %s ...", $dir);
234 5         13 eval { $CWD = $dir };
  5         17  
235 5 50       97 return [500, "Can't chdir back to $dir: $! (cwd=$CWD)"]
236             if $@;
237             }
238             }
239              
240             log_trace("Creating %s/%s%s ...",
241 33 100       127 $p, $e->{name}, $e->{is_dir} ? "/":"");
242 33 100       94 if ($e->{is_dir}) {
    50          
243 8 50       9 do {
244 8 50       13 if (defined $e->{perm}) {
245 0         0 mkdir($e->{name}, $e->{perm});
246             } else {
247 8         274 mkdir($e->{name});
248             }
249             } or return [500, "Can't create directory $p/$e->{name}: $!"];
250 8         31 $dirs[$e->{level}] = $e->{name};
251             } elsif ($e->{is_symlink}) {
252             symlink($e->{symlink_target}, $e->{name})
253 0 0       0 or return [500, "Can't create symlink $p/$e->{name} -> ".
254             "$e->{symlink_target}: $!"];
255             } else {
256             open my($fh), ">", $e->{name}
257 25 50       961 or return [500, "Can't create file $p/$e->{name}: $!"];
258 25 50       79 if (defined $e->{content}) {
259             print $fh $e->{content}
260 0 0       0 or return [500, "Can't write content to file ".
261             "$p/$e->{name}: $!"];
262             }
263 25 50       230 if (defined $e->{perm}) {
264             chmod($e->{perm}, $e->{name})
265 0 0       0 or return [500, "Can't chmod file $p/$e->{name}: $!"];
266             }
267             }
268              
269 33 50 33     138 if (defined($e->{user}) || defined($e->{group})) {
270 0         0 my %opts;
271 0 0       0 $opts{deref} = 0 if $e->{is_symlink};
272             File::chown::chown(\%opts, $e->{user}, $e->{group}, $e->{name})
273 0 0       0 or return [500, "Can't chown file $p/$e->{name}: $!"];
274             }
275              
276 33         53 $prev_level = $e->{level};
277             }
278              
279 1         7 [200, "OK"];
280             }
281              
282             $SPEC{check_layout} = {
283             v => 1.1,
284             summary => 'Check whether layout has syntax errors',
285             args => {
286             %arg_layout,
287             },
288             };
289             sub check_layout {
290 0     0 1   my %args = @_;
291              
292 0           eval { _parse_layout($args{layout}) };
  0            
293 0           my $err = $@;
294 0 0         [200, "OK", $err ? 0:1, {'func.error' => $err}];
295             }
296              
297             $SPEC{parse_layout} = {
298             v => 1.1,
299             summary => 'Parse layout string into a data structure '.
300             'suitable for processing',
301             args => {
302             %arg_layout,
303             },
304             };
305             sub parse_layout {
306 0     0 1   my %args = @_;
307              
308 0           my $res;
309 0           eval { $res = _parse_layout($args{layout}) };
  0            
310 0 0         return [400, "Layout has error(s): $@"] if $@;
311 0           [200, "OK", $res];
312             }
313              
314             1;
315             # ABSTRACT: Quickly create files/directories according to a layout
316              
317             __END__