1####################################################################################################################################
2# C Storage Interface
3####################################################################################################################################
4package pgBackRestTest::Common::StorageRepo;
5use parent 'pgBackRestTest::Common::StorageBase';
6
7use strict;
8use warnings FATAL => qw(all);
9use Carp qw(confess);
10use English '-no_match_vars';
11
12use Digest::SHA qw(sha1_hex);
13use Exporter qw(import);
14    our @EXPORT = qw();
15use File::Basename qw(dirname);
16use Fcntl qw(:mode);
17use File::stat qw{lstat};
18use JSON::PP;
19
20use pgBackRestDoc::Common::Exception;
21use pgBackRestDoc::Common::Log;
22use pgBackRestDoc::ProjectInfo;
23
24use pgBackRestTest::Common::Io::Handle;
25use pgBackRestTest::Common::Io::Process;
26use pgBackRestTest::Common::StorageBase;
27
28####################################################################################################################################
29# Temp file extension
30####################################################################################################################################
31use constant STORAGE_TEMP_EXT                                       => PROJECT_EXE . '.tmp';
32    push @EXPORT, qw(STORAGE_TEMP_EXT);
33
34####################################################################################################################################
35# new
36####################################################################################################################################
37sub new
38{
39    my $class = shift;
40
41    # Create the class hash
42    my $self = {};
43    bless $self, $class;
44
45    # Assign function parameters, defaults, and log debug info
46    (
47        my $strOperation,
48        $self->{strCommand},
49        $self->{strType},
50        $self->{lBufferMax},
51        $self->{iTimeoutIo},
52        $self->{iRepo},
53        $self->{strDefaultPathMode},
54        $self->{strDefaultFileMode},
55    ) =
56        logDebugParam
57        (
58            __PACKAGE__ . '->new', \@_,
59            {name => 'strCommand'},
60            {name => 'strType'},
61            {name => 'lBufferMax'},
62            {name => 'iTimeoutIo'},
63            {name => 'iRepo'},
64            {name => 'strDefaultPathMode', optional => true, default => '0750'},
65            {name => 'strDefaultFileMode', optional => true, default => '0640'},
66        );
67
68    # Create JSON object
69    $self->{oJSON} = JSON::PP->new()->allow_nonref();
70
71    # Return from function and log return values if any
72    return logDebugReturn
73    (
74        $strOperation,
75        {name => 'self', value => $self}
76    );
77}
78
79####################################################################################################################################
80# Escape characteres that have special meaning on the command line
81####################################################################################################################################
82sub escape
83{
84    my $self = shift;
85
86    # Assign function parameters, defaults, and log debug info
87    my
88    (
89        $strOperation,
90        $strValue,
91    ) =
92        logDebugParam
93        (
94            __PACKAGE__ . '->escape', \@_,
95            {name => 'strValue', trace => true},
96        );
97
98    $strValue =~ s/\\/\\\\/g;
99    $strValue =~ s/\</\\\</g;
100    $strValue =~ s/\>/\\\>/g;
101    $strValue =~ s/\!/\\\!/g;
102    $strValue =~ s/\*/\\\*/g;
103    $strValue =~ s/\(/\\\(/g;
104    $strValue =~ s/\)/\\\)/g;
105    $strValue =~ s/\&/\\\&/g;
106    $strValue =~ s/\'/\\\'/g;
107    $strValue =~ s/\;/\\\;/g;
108    $strValue =~ s/\?/\\\?/g;
109
110    # Return from function and log return values if any
111    return logDebugReturn
112    (
113        $strOperation,
114        {name => 'strValue', value => $strValue},
115    );
116}
117
118####################################################################################################################################
119# Execute command and return the output
120####################################################################################################################################
121sub exec
122{
123    my $self = shift;
124
125    # Assign function parameters, defaults, and log debug info
126    my
127    (
128        $strOperation,
129        $strCommand,
130    ) =
131        logDebugParam
132        (
133            __PACKAGE__ . '->exec', \@_,
134            {name => 'strCommand'},
135        );
136
137    $strCommand = "$self->{strCommand} ${strCommand}";
138    my $oBuffer = new pgBackRestTest::Common::Io::Buffered(
139        new pgBackRestTest::Common::Io::Handle($strCommand), $self->{iTimeoutIo}, $self->{lBufferMax});
140    my $oProcess = new pgBackRestTest::Common::Io::Process($oBuffer, $strCommand);
141
142    my $tResult;
143
144    while (!$oBuffer->eof())
145    {
146        $oBuffer->read(\$tResult, $self->{lBufferMax}, false);
147    }
148
149    $oProcess->close();
150
151    # Return from function and log return values if any
152    return logDebugReturn
153    (
154        $strOperation,
155        {name => 'tResult', value => $tResult},
156        {name => 'iExitStatus', value => $oProcess->exitStatus()},
157    );
158}
159
160####################################################################################################################################
161# Create storage
162####################################################################################################################################
163sub create
164{
165    my $self = shift;
166
167    # Assign function parameters, defaults, and log debug info
168    my ($strOperation) = logDebugParam(__PACKAGE__ . '->create');
169
170    $self->exec("--repo=$self->{iRepo} repo-create");
171
172    # Return from function and log return values if any
173    return logDebugReturn($strOperation);
174}
175
176####################################################################################################################################
177# Check if file exists (not a path)
178####################################################################################################################################
179sub exists
180{
181    my $self = shift;
182
183    # Assign function parameters, defaults, and log debug info
184    my
185    (
186        $strOperation,
187        $strFileExp,
188    ) =
189        logDebugParam
190        (
191            __PACKAGE__ . '->exists', \@_,
192            {name => 'strFileExp'},
193        );
194
195    # Return from function and log return values if any
196    return logDebugReturn
197    (
198        $strOperation,
199        {name => 'bExists', value => $self->info($strFileExp, {bIgnoreMissing => true})->{type} eq 'f'}
200    );
201}
202
203####################################################################################################################################
204# Read a buffer from storage all at once
205####################################################################################################################################
206sub get
207{
208    my $self = shift;
209
210    # Assign function parameters, defaults, and log debug info
211    my
212    (
213        $strOperation,
214        $xFile,
215        $strCipherPass,
216        $bRaw,
217    ) =
218        logDebugParam
219        (
220            __PACKAGE__ . '->get', \@_,
221            {name => 'xFile', required => false},
222            {name => 'strCipherPass', optional => true, redact => true},
223            {name => 'bRaw', optional => true, default => false},
224        );
225
226    # If openRead() was called first set values from that call
227    my $strFile = $xFile;
228    my $bIgnoreMissing = false;
229
230    if (ref($xFile))
231    {
232        $strFile = $xFile->{strFile};
233        $bIgnoreMissing = $xFile->{bIgnoreMissing};
234        $strCipherPass = $xFile->{strCipherPass};
235    }
236
237    # Check invalid params
238    if ($bRaw && defined($strCipherPass))
239    {
240        confess &log(ERROR, 'bRaw and strCipherPass cannot both be set');
241    }
242
243    # Get file
244    my ($tResult, $iExitStatus) = $self->exec(
245        (defined($strCipherPass) ? ' --cipher-pass=' . $self->escape($strCipherPass) : '') . ($bRaw ? ' --raw' : '') .
246        ($bIgnoreMissing ? ' --ignore-missing' : '') . " --repo=$self->{iRepo} repo-get " . $self->escape($strFile));
247
248    # Error if missing an not ignored
249    if ($iExitStatus == 1 && !$bIgnoreMissing)
250    {
251        confess &log(ERROR, "unable to open '${strFile}'", ERROR_FILE_OPEN);
252    }
253
254    # Return from function and log return values if any
255    return logDebugReturn
256    (
257        $strOperation,
258        {name => 'rtContent', value => $iExitStatus == 0 ? \$tResult : undef, trace => true},
259    );
260}
261
262####################################################################################################################################
263# Get information for path/file
264####################################################################################################################################
265sub info
266{
267    my $self = shift;
268
269    # Assign function parameters, defaults, and log debug info
270    my
271    (
272        $strOperation,
273        $strPathFileExp,
274        $bIgnoreMissing,
275    ) =
276        logDebugParam
277        (
278            __PACKAGE__ . '->info', \@_,
279            {name => 'strPathFileExp'},
280            {name => 'bIgnoreMissing', optional => true, default => false},
281        );
282
283    # Return from function and log return values if any
284    return logDebugReturn
285    (
286        $strOperation,
287        {name => 'rhInfo', value => $self->manifest($strPathFileExp, {bRecurse => false})->{'.'}, trace => true}
288    );
289}
290
291####################################################################################################################################
292# List all files/paths in path
293####################################################################################################################################
294sub list
295{
296    my $self = shift;
297
298    # Assign function parameters, defaults, and log debug info
299    my
300    (
301        $strOperation,
302        $strPathExp,
303        $strExpression,
304        $strSortOrder,
305        $bIgnoreMissing,
306    ) =
307        logDebugParam
308        (
309            __PACKAGE__ . '->list', \@_,
310            {name => 'strPathExp', required => false},
311            {name => 'strExpression', optional => true},
312            {name => 'strSortOrder', optional => true, default => 'forward'},
313            {name => 'bIgnoreMissing', optional => true, default => false},
314        );
315
316    # Get file list
317    my $rstryFileList = [];
318    my $rhManifest = $self->manifest($strPathExp, {bRecurse => false});
319
320    foreach my $strKey ($strSortOrder eq 'reverse' ? sort {$b cmp $a} keys(%{$rhManifest}) : sort keys(%{$rhManifest}))
321    {
322        next if $strKey eq '.';
323        next if defined($strExpression) && $strKey !~ $strExpression;
324
325        push(@{$rstryFileList}, $strKey);
326    }
327
328    # Return from function and log return values if any
329    return logDebugReturn
330    (
331        $strOperation,
332        {name => 'stryFileList', value => $rstryFileList}
333    );
334}
335
336####################################################################################################################################
337# Build path/file/link manifest starting with base path and including all subpaths
338####################################################################################################################################
339sub manifest
340{
341    my $self = shift;
342
343    # Assign function parameters, defaults, and log debug info
344    my
345    (
346        $strOperation,
347        $strPathExp,
348        $bRecurse,
349    ) =
350        logDebugParam
351        (
352            __PACKAGE__ . '->manifest', \@_,
353            {name => 'strPathExp'},
354            {name => 'bRecurse', optional => true, default => true},
355        );
356
357    my $rhManifest = $self->{oJSON}->decode(
358        $self->exec(
359            "--output=json" . ($bRecurse ? ' --recurse' : '') . " --repo=$self->{iRepo} repo-ls " . $self->escape($strPathExp)));
360
361    # Transform the manifest to the old format
362    foreach my $strKey (keys(%{$rhManifest}))
363    {
364        if ($rhManifest->{$strKey}{type} eq 'file')
365        {
366            $rhManifest->{$strKey}{type} = 'f';
367
368            if (defined($rhManifest->{$strKey}{time}))
369            {
370                $rhManifest->{$strKey}{modified_time} = $rhManifest->{$strKey}{time};
371                delete($rhManifest->{$strKey}{time});
372            }
373        }
374        elsif ($rhManifest->{$strKey}{type} eq 'path')
375        {
376            $rhManifest->{$strKey}{type} = 'd';
377        }
378        elsif ($rhManifest->{$strKey}{type} eq 'link')
379        {
380            $rhManifest->{$strKey}{type} = 'l';
381        }
382        elsif ($rhManifest->{$strKey}{type} eq 'special')
383        {
384            $rhManifest->{$strKey}{type} = 's';
385        }
386        else
387        {
388            confess "invalid file type '$rhManifest->{type}'";
389        }
390    }
391
392    # Return from function and log return values if any
393    return logDebugReturn
394    (
395        $strOperation,
396        {name => 'rhManifest', value => $rhManifest, trace => true}
397    );
398}
399
400####################################################################################################################################
401# Open file for reading
402####################################################################################################################################
403sub openRead
404{
405    my $self = shift;
406
407    # Assign function parameters, defaults, and log debug info
408    my
409    (
410        $strOperation,
411        $strFile,
412        $bIgnoreMissing,
413        $strCipherPass,
414    ) =
415        logDebugParam
416        (
417            __PACKAGE__ . '->openRead', \@_,
418            {name => 'strFile'},
419            {name => 'bIgnoreMissing', optional => true, default => false},
420            {name => 'strCipherPass', optional => true, redact => true},
421        );
422
423    # Return from function and log return values if any
424    return logDebugReturn
425    (
426        $strOperation,
427        {name => 'rhFileIo', value => {strFile => $strFile, bIgnoreMissing => $bIgnoreMissing, strCipherPass => $strCipherPass},
428            trace => true},
429    );
430}
431
432####################################################################################################################################
433# Remove path and all files below it
434####################################################################################################################################
435sub pathRemove
436{
437    my $self = shift;
438
439    # Assign function parameters, defaults, and log debug info
440    my
441    (
442        $strOperation,
443        $strPath,
444        $bRecurse,
445    ) =
446        logDebugParam
447        (
448            __PACKAGE__ . '->pathRemove', \@_,
449            {name => 'strPath'},
450            {name => 'bRecurse', optional => true, default => false},
451        );
452
453    $self->exec("--repo=$self->{iRepo} repo-rm " . ($bRecurse ? '--recurse ' : '') . $self->escape($strPath));
454
455    # Return from function and log return values if any
456    return logDebugReturn($strOperation);
457}
458
459####################################################################################################################################
460# put - writes a buffer out to storage all at once
461####################################################################################################################################
462sub put
463{
464    my $self = shift;
465
466    # Assign function parameters, defaults, and log debug info
467    my
468    (
469        $strOperation,
470        $strFile,
471        $tContent,
472        $strCipherPass,
473        $bRaw,
474    ) =
475        logDebugParam
476        (
477            __PACKAGE__ . '->put', \@_,
478            {name => 'strFile'},
479            {name => 'tContent', required => false},
480            {name => 'strCipherPass', optional => true, redact => true},
481            {name => 'bRaw', optional => true, default => false},
482        );
483
484    # Check invalid params
485    if ($bRaw && defined($strCipherPass))
486    {
487        confess &log(ERROR, 'bRaw and strCipherPass cannot both be set');
488    }
489
490    # Put file
491    my $strCommand =
492        "$self->{strCommand}" . (defined($strCipherPass) ? ' --cipher-pass=' . $self->escape($strCipherPass) : '') .
493            ($bRaw ? ' --raw' : '') . " --repo=$self->{iRepo} repo-put " . $self->escape($strFile);
494
495    my $oBuffer = new pgBackRestTest::Common::Io::Buffered(
496        new pgBackRestTest::Common::Io::Handle($strCommand), $self->{iTimeoutIo}, $self->{lBufferMax});
497    my $oProcess = new pgBackRestTest::Common::Io::Process($oBuffer, $strCommand);
498
499    if (defined($tContent))
500    {
501        $oBuffer->write(\$tContent);
502    }
503
504    close($oBuffer->handleWrite());
505
506    my $tResult;
507
508    while (!$oBuffer->eof())
509    {
510        $oBuffer->read(\$tResult, $self->{lBufferMax}, false);
511    }
512
513    close($oBuffer->handleRead());
514    $oProcess->close();
515
516    # Return from function and log return values if any
517    return logDebugReturn($strOperation);
518}
519
520####################################################################################################################################
521# Remove file
522####################################################################################################################################
523sub remove
524{
525    my $self = shift;
526
527    # Assign function parameters, defaults, and log debug info
528    my
529    (
530        $strOperation,
531        $strFile,
532    ) =
533        logDebugParam
534        (
535            __PACKAGE__ . '->remove', \@_,
536            {name => 'xFileExp'},
537        );
538
539    $self->exec("--repo=$self->{iRepo} repo-rm " . $self->escape($strFile));
540
541    # Return from function and log return values if any
542    return logDebugReturn($strOperation);
543}
544
545####################################################################################################################################
546# Cache storage so it can be retrieved quickly
547####################################################################################################################################
548my $oRepoStorage;
549
550####################################################################################################################################
551# storageRepoCommandSet
552####################################################################################################################################
553my $strStorageRepoCommand;
554my $strStorageRepoType;
555
556sub storageRepoCommandSet
557{
558    # Assign function parameters, defaults, and log debug info
559    my
560    (
561        $strOperation,
562        $strCommand,
563        $strStorageType,
564    ) =
565        logDebugParam
566        (
567            __PACKAGE__ . '::storageRepoCommandSet', \@_,
568            {name => 'strCommand'},
569            {name => 'strStorageType'},
570        );
571
572    $strStorageRepoCommand = $strCommand;
573    $strStorageRepoType = $strStorageType;
574
575    # Return from function and log return values if any
576    return logDebugReturn($strOperation);
577}
578
579push @EXPORT, qw(storageRepoCommandSet);
580
581####################################################################################################################################
582# storageRepo - get repository storage
583####################################################################################################################################
584sub storageRepo
585{
586    # Assign function parameters, defaults, and log debug info
587    my
588    (
589        $strOperation,
590        $strStanza,
591        $iRepo,
592    ) =
593        logDebugParam
594        (
595            __PACKAGE__ . '::storageRepo', \@_,
596            {name => 'strStanza', optional => true, trace => true},
597            {name => 'iRepo', optional => true, default => 1, trace => true},
598        );
599
600    # Create storage if not defined
601    if (!defined($oRepoStorage->{$iRepo}))
602    {
603        $oRepoStorage->{$iRepo} = new pgBackRestTest::Common::StorageRepo(
604            $strStorageRepoCommand, $strStorageRepoType, 64 * 1024, 60, $iRepo);
605    }
606
607    # Return from function and log return values if any
608    return logDebugReturn
609    (
610        $strOperation,
611        {name => 'oStorageRepo', value => $oRepoStorage->{$iRepo}, trace => true},
612    );
613}
614
615push @EXPORT, qw(storageRepo);
616
617####################################################################################################################################
618# Getters
619####################################################################################################################################
620sub capability {shift->type() eq STORAGE_POSIX}
621sub type {shift->{strType}}
622
6231;
624