1 | #!/usr/bin/perl |
---|
2 | $Version = 'MythTV Recording Archiver v0.3'; |
---|
3 | |
---|
4 | # MythTV Recording Archiver |
---|
5 | # |
---|
6 | # Written by Mark R. Buechler 03/01/07 |
---|
7 | # License: GPL2 |
---|
8 | |
---|
9 | sub usage |
---|
10 | { |
---|
11 | die <<"EndUsage"; |
---|
12 | $Version |
---|
13 | |
---|
14 | Usage: |
---|
15 | |
---|
16 | Required: (at least one of) |
---|
17 | -archive : Run in archive mode. |
---|
18 | -cleanup <mode> : Run in cleanup mode where <mode> is a comma seperated |
---|
19 | list of abandoned, zerobyte, metadata, deleted or all |
---|
20 | |
---|
21 | Archive Arguments: |
---|
22 | -from <group> : Archive recordings from storage group <group>. |
---|
23 | -to <group> : Archive recordings to storage group <group>. |
---|
24 | |
---|
25 | Archive Options: |
---|
26 | -ignorelivetv : Ignore LiveTV recordings. |
---|
27 | -hostname <host> : Only archive recordings which exist on |
---|
28 | Myth backend <host>. |
---|
29 | Debug: |
---|
30 | -debug : Don't archive or delete anything. |
---|
31 | |
---|
32 | Cleanup Modes: |
---|
33 | abandoned - Delete recordings with missing metadata. |
---|
34 | zerobyte - Delete all zero-byte recordings and associated metadata. |
---|
35 | metadata - Delete metadata for recordings which don't exist. |
---|
36 | deleted - Delete recordins and metadata which exist in the 'Deleted' |
---|
37 | storage group. |
---|
38 | all - Perform all cleanup types. |
---|
39 | |
---|
40 | Examples: |
---|
41 | Archive recordings from the LiveTV group to the Default group: |
---|
42 | myth_archive.pl -archive -from LiveTV -to Default -ignorelivetv |
---|
43 | |
---|
44 | Perform some cleanup of orphoned (abandoned) recordings: |
---|
45 | myth_archive.pl -cleanup abandoned |
---|
46 | |
---|
47 | Notes: |
---|
48 | If -hostname option is not used, you must ensure that storage group |
---|
49 | paths are identical across all MythTV backends. |
---|
50 | |
---|
51 | EndUsage |
---|
52 | } |
---|
53 | |
---|
54 | use Getopt::Long; |
---|
55 | use Carp qw(cluck); |
---|
56 | use IO::File; |
---|
57 | use DirHandle; |
---|
58 | use DBI; |
---|
59 | |
---|
60 | use File::Copy; |
---|
61 | use strict; |
---|
62 | |
---|
63 | # Pull in the MythTV module |
---|
64 | #use lib '/opt/mns/mythtv/share/perl'; |
---|
65 | use MythTV; |
---|
66 | |
---|
67 | my $TRUE = 1; |
---|
68 | my $FALSE = 0; |
---|
69 | |
---|
70 | my $LIVETV_AUTOEXPIRE = 9999; |
---|
71 | my $DELETED_RECGROUP = 'Deleted'; |
---|
72 | |
---|
73 | # Operational modes |
---|
74 | my $MODE_ABANDONED = 0x01; |
---|
75 | my $MODE_ZEROBYTE = 0x02; |
---|
76 | my $MODE_METADATA = 0x04; |
---|
77 | my $MODE_DELETED = 0x08; |
---|
78 | my $MODE_ALL = 0x0f; |
---|
79 | my $MODE_ARCHIVE = 0x10; |
---|
80 | |
---|
81 | my $DBH; |
---|
82 | my $MYTH; |
---|
83 | my $_DEBUG_; |
---|
84 | |
---|
85 | &main(); |
---|
86 | |
---|
87 | sub getArgs { |
---|
88 | my $modes; |
---|
89 | my $from; |
---|
90 | my $to; |
---|
91 | my $hostname; |
---|
92 | my $ignorelivetv; |
---|
93 | my $cleanupmode; |
---|
94 | my $archivemode; |
---|
95 | my $batchmode; |
---|
96 | my $force; |
---|
97 | my $debug; |
---|
98 | |
---|
99 | my $p = new Getopt::Long::Parser; |
---|
100 | |
---|
101 | if (!$p->getoptions('from=s' => \$from, |
---|
102 | 'to=s' => \$to, |
---|
103 | 'hostname=s' => \$hostname, |
---|
104 | 'ignorelivetv' => \$ignorelivetv, |
---|
105 | 'archive' => \$archivemode, |
---|
106 | 'cleanup=s' => \$cleanupmode, |
---|
107 | 'batch' => \$batchmode, |
---|
108 | 'force' => \$force, |
---|
109 | 'debug' => \$debug)) { |
---|
110 | print "Invalid parameters given.\n"; |
---|
111 | usage(); |
---|
112 | } |
---|
113 | |
---|
114 | $force = $TRUE if (defined($force)); |
---|
115 | $ignorelivetv = $TRUE if (defined($ignorelivetv)); |
---|
116 | $batchmode = $TRUE if (defined($batchmode)); |
---|
117 | $cleanupmode =~ tr/A-Z/a-z/; |
---|
118 | $_DEBUG_ = $TRUE if (defined($debug)); |
---|
119 | |
---|
120 | if ($archivemode && (!$from || !$to)) { |
---|
121 | print "Please specify -from and -to with -archive.\n"; |
---|
122 | usage(); |
---|
123 | } |
---|
124 | |
---|
125 | if ($cleanupmode && $hostname && !$force) { |
---|
126 | print "\nDANGER: This can be a BAD idea! Use -debug before trying this!\n". |
---|
127 | "If you really want this, specify -force with -cleanup and -hostname.\n\n"; |
---|
128 | exit 1; |
---|
129 | } |
---|
130 | |
---|
131 | if ($cleanupmode) { |
---|
132 | foreach my $mode (split(/\,/, $cleanupmode)) { |
---|
133 | if ($mode eq 'abandoned') { |
---|
134 | $modes |= $MODE_ABANDONED; |
---|
135 | } elsif ($mode eq 'zerobyte') { |
---|
136 | $modes |= $MODE_ZEROBYTE; |
---|
137 | } elsif ($mode eq 'metadata') { |
---|
138 | $modes |= $MODE_METADATA; |
---|
139 | } elsif ($mode eq 'deleted') { |
---|
140 | $modes |= $MODE_DELETED; |
---|
141 | } elsif ($mode eq 'all') { |
---|
142 | $modes |= $MODE_ALL; |
---|
143 | } else { |
---|
144 | print "Invalid mode '$mode' specified.\n"; |
---|
145 | usage(); |
---|
146 | } |
---|
147 | } |
---|
148 | } |
---|
149 | |
---|
150 | if (defined($archivemode)) { |
---|
151 | $modes |= $MODE_ARCHIVE; |
---|
152 | } |
---|
153 | |
---|
154 | if (($from eq 'LiveTV') && !$ignorelivetv && !$force) { |
---|
155 | print "Your source group is $from but you are not ignoring LiveTV recordings, ". |
---|
156 | "please use -force option.\n"; |
---|
157 | exit 1; |
---|
158 | } |
---|
159 | |
---|
160 | return($modes, $from, $to, $hostname, $ignorelivetv, $batchmode); |
---|
161 | } |
---|
162 | |
---|
163 | sub main { |
---|
164 | my($modes, $from, $to, $hostname, $ignorelivetv, $batchmode) = getArgs(); |
---|
165 | my $samefs = $FALSE; |
---|
166 | my %recordingLookup; |
---|
167 | my %abandoned; |
---|
168 | |
---|
169 | print modesToString($modes); |
---|
170 | |
---|
171 | $MYTH = new MythTV(); |
---|
172 | |
---|
173 | $DBH = $MYTH->{'dbh'}; |
---|
174 | |
---|
175 | my $storage = getStorageGroups($hostname); |
---|
176 | my $recordings = getRecordings($hostname); |
---|
177 | my($stored, $cifs, $thumbs) = getStoredRecordings($storage); |
---|
178 | |
---|
179 | if ($modes & $MODE_ARCHIVE) { |
---|
180 | my $fromDir = $$storage{$from}; |
---|
181 | my $toDir = $$storage{$to}; |
---|
182 | |
---|
183 | die("FATAL: Storage group '$from' is not defined in database, aborting!\n") if (!$fromDir); |
---|
184 | die("FATAL: Storage group '$to' is not defined in database, aborting!\n") if (!$toDir); |
---|
185 | |
---|
186 | my($mpFrom, undef, undef, undef) = getMountpoint($fromDir); |
---|
187 | my($mpTo, undef, undef, undef) = getMountpoint($toDir); |
---|
188 | |
---|
189 | if ($mpFrom eq $mpTo) { |
---|
190 | print "-> From and to storage groups reside on the same filesystem, ". |
---|
191 | "moving recordings rather than copying/deleting.\n\n"; |
---|
192 | $samefs = $TRUE; |
---|
193 | } else { |
---|
194 | print "-> From and to storage groups reside on different filesystems, ". |
---|
195 | "copying/deleting recordings rather than moving.\n\n"; |
---|
196 | } |
---|
197 | } |
---|
198 | |
---|
199 | # Some recordings may have moved, figure out which ones and update them |
---|
200 | foreach my $group (keys %{$recordings}) { |
---|
201 | foreach my $recording (keys %{$$recordings{$group}}) { |
---|
202 | my $recgroup = $$recordings{$group}->{$recording}->{'RECGROUP'}; |
---|
203 | my $chanid = $$recordings{$group}->{$recording}->{'CHANID'}; |
---|
204 | my $starttime = $$recordings{$group}->{$recording}->{'START'}; |
---|
205 | my $title = $$recordings{$group}->{$recording}->{'TITLE'}; |
---|
206 | my $subtitle = $$recordings{$group}->{$recording}->{'SUBTITLE'}; |
---|
207 | |
---|
208 | $recordingLookup{$recording}++; |
---|
209 | |
---|
210 | my $_group = findRecording($storage, $recording, $chanid, $starttime); |
---|
211 | |
---|
212 | if (!defined($_group)) { |
---|
213 | if ($modes & $MODE_METADATA) { |
---|
214 | print "\tDeleting meta-data for missing recording '$recording' in group '$group'.\n"; |
---|
215 | |
---|
216 | deleteFromDb($chanid, $starttime, $recording, $recgroup); |
---|
217 | } else { |
---|
218 | print "\tStorage group '$group': problem found with recording title '$title - ". |
---|
219 | "$subtitle'.\n"; |
---|
220 | } |
---|
221 | |
---|
222 | next; |
---|
223 | } |
---|
224 | |
---|
225 | updateStorageGroup($chanid, $starttime, $recording, $group) |
---|
226 | if (defined($_group) && ($group ne $_group)); |
---|
227 | } |
---|
228 | } |
---|
229 | |
---|
230 | # Update |
---|
231 | $recordings = getRecordings($hostname); |
---|
232 | |
---|
233 | # Catch recording files still lingering around |
---|
234 | my %totalSize; |
---|
235 | foreach my $group (keys %{$stored}) { |
---|
236 | foreach my $recording (keys %{$$stored{$group}}) { |
---|
237 | if (!$recordingLookup{$recording}) { |
---|
238 | my $size = $$stored{$group}->{$recording}; |
---|
239 | $abandoned{$group}->{$recording} = $size; |
---|
240 | print "WARNING: Found recording file '$recording' in storage group '$group'\n ". |
---|
241 | "of size $size with no entry in database!\n"; |
---|
242 | $totalSize{$group} += $size; |
---|
243 | } |
---|
244 | } |
---|
245 | } |
---|
246 | |
---|
247 | foreach my $group (keys %{$cifs}) { |
---|
248 | foreach my $recording (keys %{$$cifs{$group}}) { |
---|
249 | my $size = $$cifs{$group}->{$recording}; |
---|
250 | $abandoned{$group}->{$recording} = $size; |
---|
251 | print "WARNING: Found recording file '$recording' in storage group '$group'\n ". |
---|
252 | "of size $size with no entry in database!\n"; |
---|
253 | $totalSize{$group} += $size; |
---|
254 | } |
---|
255 | } |
---|
256 | |
---|
257 | foreach my $group (keys %totalSize) { |
---|
258 | my $size = $totalSize{$group}; |
---|
259 | print "Found a total of $size bytes in storage group '$group' with no database entries!\n"; |
---|
260 | } |
---|
261 | |
---|
262 | if ($modes & $MODE_ABANDONED) { |
---|
263 | if (!$batchmode) { |
---|
264 | print "Preparing to cleanup recordings, press ctrl-c within 10 seconds to abort.\n"; |
---|
265 | sleep 10; |
---|
266 | } |
---|
267 | |
---|
268 | foreach my $group (keys %abandoned) { |
---|
269 | foreach my $recording (keys %{$abandoned{$group}}) { |
---|
270 | my $size = $abandoned{$group}->{$recording}; |
---|
271 | print "\tDeleting recording '$recording' in storage group '$group' of size $size bytes..\n"; |
---|
272 | unlinkFile($$storage{$group}, $recording); |
---|
273 | |
---|
274 | if ($$thumbs{$recording}) { |
---|
275 | foreach my $thumb (@{$$thumbs{$recording}}) { |
---|
276 | print "\tDeleting thumbnail '$thumb'..\n"; |
---|
277 | unlinkFile($$storage{$group}, $thumb); |
---|
278 | } |
---|
279 | } |
---|
280 | } |
---|
281 | } |
---|
282 | |
---|
283 | if ($modes & $MODE_DELETED) { |
---|
284 | # Take care of recordings in the 'Deleted' group. |
---|
285 | foreach my $group (keys %{$recordings}) { |
---|
286 | foreach my $recording (keys %{$$recordings{$group}}) { |
---|
287 | my $recgroup = $$recordings{$group}->{$recording}->{'RECGROUP'}; |
---|
288 | my $chanid = $$recordings{$group}->{$recording}->{'CHANID'}; |
---|
289 | my $starttime = $$recordings{$group}->{$recording}->{'START'}; |
---|
290 | |
---|
291 | if ($recgroup eq $DELETED_RECGROUP) { |
---|
292 | print "\tDeleting recording '$recording' in storage group '$group' ". |
---|
293 | "in recgroup '$recgroup'..\n"; |
---|
294 | unlinkFile($$storage{$group}, $recording); |
---|
295 | |
---|
296 | print "\tDeleting metadata for recording '$recording' in storage ". |
---|
297 | "group '$group' in recgroup '$recgroup'..\n"; |
---|
298 | deleteFromDb($chanid, $starttime, $recording, $DELETED_RECGROUP); |
---|
299 | |
---|
300 | if ($$thumbs{$recording}) { |
---|
301 | foreach my $thumb (@{$$thumbs{$recording}}) { |
---|
302 | print "\tDeleting thumbnail '$thumb'..\n"; |
---|
303 | unlinkFile($$storage{$group}, $thumb); |
---|
304 | } |
---|
305 | } |
---|
306 | } |
---|
307 | } |
---|
308 | } |
---|
309 | } |
---|
310 | } |
---|
311 | |
---|
312 | foreach my $recording (keys %{$$recordings{$from}}) { |
---|
313 | my $size = recordingSize($$storage{$from}, $recording); |
---|
314 | my $recgroup = $$recordings{$from}->{$recording}->{'RECGROUP'}; |
---|
315 | my $chanid = $$recordings{$from}->{$recording}->{'CHANID'}; |
---|
316 | my $starttime = $$recordings{$from}->{$recording}->{'START'}; |
---|
317 | my $title = $$recordings{$from}->{$recording}->{'TITLE'}; |
---|
318 | my $subtitle = $$recordings{$from}->{$recording}->{'SUBTITLE'}; |
---|
319 | my $islivetv = $$recordings{$from}->{$recording}->{'LIVETV'}; |
---|
320 | |
---|
321 | $islivetv = 'LiveTV' if ($islivetv); |
---|
322 | |
---|
323 | if ($size == 0) { |
---|
324 | if ($modes & $MODE_ZEROBYTE) { |
---|
325 | my $recgroup = $$recordings{$from}->{$recording}->{'RECGROUP'}; |
---|
326 | print "\tDeleting zero-byte recording '$recording' in storage group '$from' ". |
---|
327 | "in recgroup '$recgroup'..\n"; |
---|
328 | unlinkFile($$storage{$from}, $recording); |
---|
329 | |
---|
330 | print "\tDeleting metadata for recording '$recording' in storage group '$from' ". |
---|
331 | "in recgroup '$recgroup'..\n"; |
---|
332 | deleteFromDb($chanid, $starttime, $recording, $recgroup); |
---|
333 | |
---|
334 | if ($$thumbs{$recording}) { |
---|
335 | foreach my $thumb (@{$$thumbs{$recording}}) { |
---|
336 | print "\tDeleting thumbnail '$thumb'..\n"; |
---|
337 | unlinkFile($$storage{$from}, $thumb); |
---|
338 | } |
---|
339 | } |
---|
340 | } else { |
---|
341 | print "WARNING: File '$recording' for $islivetv recording of '$title - $subtitle' ". |
---|
342 | "is 0 bytes in size!\n"; |
---|
343 | } |
---|
344 | } elsif ($modes & $MODE_ARCHIVE) { |
---|
345 | if (!$ignorelivetv || ($ignorelivetv && !$islivetv)) { |
---|
346 | my $fromDir = $$storage{$from}; |
---|
347 | my $toDir = $$storage{$to}; |
---|
348 | |
---|
349 | print "\tArchiving recording '$recording':\n\t\tTitle '$title - $subtitle' from ". |
---|
350 | "'$fromDir' to '$toDir'..\n"; |
---|
351 | |
---|
352 | if (!archiveFile($fromDir, $toDir, $recording, $samefs)) { |
---|
353 | updateStorageGroup($chanid, $starttime, $recording, $to); |
---|
354 | } |
---|
355 | |
---|
356 | if ($$thumbs{$recording}) { |
---|
357 | foreach my $thumb (@{$$thumbs{$recording}}) { |
---|
358 | archiveFile($fromDir, $toDir, $thumb, $samefs); |
---|
359 | } |
---|
360 | } |
---|
361 | } |
---|
362 | } |
---|
363 | } |
---|
364 | |
---|
365 | # Now print some stats.. |
---|
366 | print "\n\tStorage Group\tSize\tPortion\n"; |
---|
367 | print "\t----------------------------------------\n"; |
---|
368 | |
---|
369 | my $total = 0; |
---|
370 | foreach my $group (keys %{$recordings}) { |
---|
371 | next if (!$group); |
---|
372 | my $group_s = 0; |
---|
373 | my(undef, $mp_size, undef, undef) = getMountpoint($$storage{$group}); |
---|
374 | $mp_size = $mp_size * 1024; |
---|
375 | $mp_size -= (.05 * $mp_size); |
---|
376 | |
---|
377 | foreach my $recording (keys %{$$recordings{$group}}) { |
---|
378 | my $size = recordingSize($$storage{$group}, $recording); |
---|
379 | $group_s += $size; |
---|
380 | } |
---|
381 | |
---|
382 | my $portion = round((($group_s / $mp_size) * 100), 2) if ($mp_size); |
---|
383 | |
---|
384 | print "\t$group\t\t$group_s\t$portion%\n"; |
---|
385 | $total += $group_s; |
---|
386 | } |
---|
387 | |
---|
388 | print "\t----------------------------------------\n"; |
---|
389 | print "\tTotal:\t\t$total\n\n"; |
---|
390 | |
---|
391 | print "All done!\n"; |
---|
392 | } |
---|
393 | |
---|
394 | sub getStorageGroups { |
---|
395 | my $hostname = shift; |
---|
396 | my %groups; |
---|
397 | my $where; |
---|
398 | |
---|
399 | $where = "WHERE hostname = '$hostname'" if ($hostname); |
---|
400 | |
---|
401 | my $sth = $DBH->prepare("SELECT groupname, dirname FROM storagegroup $where"); |
---|
402 | $sth->execute(); |
---|
403 | |
---|
404 | while (my $row = $sth->fetchrow_hashref()) { |
---|
405 | my $path = $row->{'dirname'}; |
---|
406 | $path =~ s/\/+$//; |
---|
407 | |
---|
408 | $groups{$row->{'groupname'}} = $path; |
---|
409 | } |
---|
410 | |
---|
411 | $sth->finish(); |
---|
412 | |
---|
413 | return \%groups; |
---|
414 | } |
---|
415 | |
---|
416 | sub getRecordings { |
---|
417 | my $hostname = shift; |
---|
418 | my %recordings; |
---|
419 | my $where; |
---|
420 | |
---|
421 | $where = "WHERE hostname = '$hostname'" if ($hostname); |
---|
422 | |
---|
423 | my $sth = $DBH->prepare("SELECT chanid, starttime, basename, title, subtitle, ". |
---|
424 | "autoexpire, storagegroup, recgroup FROM recorded $where"); |
---|
425 | $sth->execute(); |
---|
426 | |
---|
427 | while (my $row = $sth->fetchrow_hashref()) { |
---|
428 | $recordings{$row->{'storagegroup'}}->{$row->{'basename'}}->{'TITLE'} = |
---|
429 | $row->{'title'}; |
---|
430 | $recordings{$row->{'storagegroup'}}->{$row->{'basename'}}->{'SUBTITLE'} = |
---|
431 | $row->{'subtitle'}; |
---|
432 | $recordings{$row->{'storagegroup'}}->{$row->{'basename'}}->{'CHANID'} = |
---|
433 | $row->{'chanid'}; |
---|
434 | $recordings{$row->{'storagegroup'}}->{$row->{'basename'}}->{'START'} = |
---|
435 | $row->{'starttime'}; |
---|
436 | $recordings{$row->{'storagegroup'}}->{$row->{'basename'}}->{'RECGROUP'} = |
---|
437 | $row->{'recgroup'}; |
---|
438 | $recordings{$row->{'storagegroup'}}->{$row->{'basename'}}->{'LIVETV'} = |
---|
439 | ($row->{'autoexpire'} >= $LIVETV_AUTOEXPIRE) ? $TRUE : $FALSE; |
---|
440 | } |
---|
441 | |
---|
442 | $sth->finish(); |
---|
443 | |
---|
444 | return \%recordings; |
---|
445 | } |
---|
446 | |
---|
447 | sub getStoredRecordings { |
---|
448 | my $storage = shift; |
---|
449 | my %stored; |
---|
450 | my %cifs; |
---|
451 | my %thumbs; |
---|
452 | |
---|
453 | foreach my $group (keys %{$storage}) { |
---|
454 | my $path = $$storage{$group}; |
---|
455 | my $dir = new DirHandle($path); |
---|
456 | |
---|
457 | die("FATAL: Directory '$path' for storage group '$group' does not exist!\n") |
---|
458 | if (!defined($dir)); |
---|
459 | |
---|
460 | while (defined(my $entry = $dir->read())) { |
---|
461 | next if (-d $entry); |
---|
462 | next if (($entry !~ /\.mpg$/i) && |
---|
463 | ($entry !~ /\.nuv$/i) && |
---|
464 | ($entry !~ /\.png$/i) && |
---|
465 | ($entry !~ /^cifs/)); |
---|
466 | |
---|
467 | if ($entry =~ /\.mpg$/) { |
---|
468 | $stored{$group}->{$entry} = recordingSize($path, $entry); |
---|
469 | } elsif ($entry =~ /\.png$/) { |
---|
470 | if ($entry =~ /^(.+\.(mpg|nuv))\./i) { |
---|
471 | push @{$thumbs{$1}}, $entry; |
---|
472 | } |
---|
473 | } elsif ($entry =~ /^cifs/) { |
---|
474 | $cifs{$group}->{$entry} = recordingSize($path, $entry); |
---|
475 | } |
---|
476 | } |
---|
477 | } |
---|
478 | |
---|
479 | return(\%stored, \%cifs, \%thumbs); |
---|
480 | } |
---|
481 | |
---|
482 | sub updateStorageGroup { |
---|
483 | my $chanid = shift; |
---|
484 | my $starttime = shift; |
---|
485 | my $recording = shift; |
---|
486 | my $group = shift; |
---|
487 | |
---|
488 | my $sql = "UPDATE recorded SET storagegroup = '$group' ". |
---|
489 | "WHERE chanid = $chanid AND starttime = '$starttime' AND basename = '$recording'"; |
---|
490 | |
---|
491 | if ($_DEBUG_) { |
---|
492 | print "DBG: updateStorageGroup(): $sql\n"; |
---|
493 | } else { |
---|
494 | my $sth = $DBH->prepare($sql); |
---|
495 | $sth->execute(); |
---|
496 | $sth->finish(); |
---|
497 | } |
---|
498 | } |
---|
499 | |
---|
500 | sub findRecording { |
---|
501 | my $storage = shift; |
---|
502 | my $recording = shift; |
---|
503 | my $chanid = shift; |
---|
504 | my $starttime = shift; |
---|
505 | my $found; |
---|
506 | |
---|
507 | foreach my $group (keys %{$storage}) { |
---|
508 | my $directory = $$storage{$group}; |
---|
509 | |
---|
510 | if (-e "$directory/$recording") { |
---|
511 | if ($found) { |
---|
512 | print "WARNING: Found recording '$recording' in multiple groups, ignoring!\n"; |
---|
513 | return undef; |
---|
514 | } |
---|
515 | |
---|
516 | $found = $group; |
---|
517 | } |
---|
518 | } |
---|
519 | |
---|
520 | if (!$found) { |
---|
521 | print "WARNING: Unable to find recording '$recording' in any storage group!\n"; |
---|
522 | return undef; |
---|
523 | } |
---|
524 | |
---|
525 | return $found; |
---|
526 | } |
---|
527 | |
---|
528 | sub deleteFromDb { |
---|
529 | my $chanid = shift; |
---|
530 | my $starttime = shift; |
---|
531 | my $basename = shift; |
---|
532 | my $recgroup = shift; |
---|
533 | |
---|
534 | doSql("DELETE FROM recorded WHERE chanid = $chanid AND starttime = '$starttime' AND ". |
---|
535 | "basename = '$basename' AND recgroup = '$recgroup'"); |
---|
536 | doSql("DELETE FROM recordedcredits WHERE chanid = $chanid AND starttime = '$starttime'"); |
---|
537 | doSql("DELETE FROM recordedmarkup WHERE chanid = $chanid AND starttime = '$starttime'"); |
---|
538 | doSql("DELETE FROM recordedprogram WHERE chanid = $chanid AND starttime = '$starttime'"); |
---|
539 | doSql("DELETE FROM recordedrating WHERE chanid = $chanid AND starttime = '$starttime'"); |
---|
540 | doSql("DELETE FROM recordedseek WHERE chanid = $chanid AND starttime = '$starttime'"); |
---|
541 | } |
---|
542 | |
---|
543 | sub archiveFile { |
---|
544 | my $fromDir = shift; |
---|
545 | my $toDir = shift; |
---|
546 | my $file = shift; |
---|
547 | my $samefs = shift; |
---|
548 | |
---|
549 | if (!-e "$fromDir/$file") { |
---|
550 | print "WARNING: Attempting to archive the file '$fromDir/$file' which doesn't exist!\n"; |
---|
551 | return $TRUE; |
---|
552 | } |
---|
553 | |
---|
554 | if (-e "$toDir/$file") { |
---|
555 | print "WARNING: Recording '$file' already exists in destination group, ignoring!\n"; |
---|
556 | return $TRUE; |
---|
557 | } |
---|
558 | |
---|
559 | if (!$samefs && !checkForSpace($fromDir, $toDir, $file)) { |
---|
560 | print "Storage group directory '$toDir' is too full to achive recording '$file', ignoring.\n"; |
---|
561 | return $TRUE; |
---|
562 | } |
---|
563 | |
---|
564 | if ($_DEBUG_) { |
---|
565 | print "DBG: Would archive '$fromDir/$file' to '$toDir/$file' mode ".($samefs ? "mv" : "cp")."\n"; |
---|
566 | } else { |
---|
567 | if ($samefs) { |
---|
568 | if (!move("$fromDir/$file", "$toDir/$file")) { |
---|
569 | print "WARNING: Failed to archive/move recording $file from $fromDir to $toDir: $!\n"; |
---|
570 | return $TRUE; |
---|
571 | } |
---|
572 | } else { |
---|
573 | if (!copy("$fromDir/$file", "$toDir/$file")) { |
---|
574 | print "WARNING: Failed to archive/copy recording $file from $fromDir to $toDir: $!\n"; |
---|
575 | return $TRUE; |
---|
576 | } |
---|
577 | } |
---|
578 | } |
---|
579 | |
---|
580 | my $orig_size = recordingSize($fromDir, $file); |
---|
581 | my $copy_size = recordingSize($toDir, $file); |
---|
582 | |
---|
583 | if (!$samefs && ($orig_size != $copy_size)) { |
---|
584 | print "WARNING: Failed to copy recording '$file' from $fromDir to $toDir, file sizes don't match!\n" |
---|
585 | if (!$_DEBUG_); |
---|
586 | return $TRUE; |
---|
587 | } elsif (!$samefs) { |
---|
588 | unlinkFile($fromDir, $file); |
---|
589 | } |
---|
590 | |
---|
591 | return $FALSE; |
---|
592 | } |
---|
593 | |
---|
594 | sub unlinkFile { |
---|
595 | my $fromDir = shift; |
---|
596 | my $file = shift; |
---|
597 | |
---|
598 | return $TRUE if (!$fromDir || !$file); |
---|
599 | |
---|
600 | if (-e "$fromDir/$file") { |
---|
601 | if ($_DEBUG_) { |
---|
602 | print "DBG: Would unlink \"$fromDir/$file\"\n"; |
---|
603 | } else { |
---|
604 | if (!unlink "$fromDir/$file") { |
---|
605 | print "WARNING: Failed to unlink file $file in $fromDir: $!\n"; |
---|
606 | return $TRUE; |
---|
607 | } |
---|
608 | } |
---|
609 | } |
---|
610 | |
---|
611 | return $FALSE; |
---|
612 | } |
---|
613 | |
---|
614 | sub isFileInUse { |
---|
615 | my $file = shift; |
---|
616 | |
---|
617 | my $result = `fuser $file`; |
---|
618 | |
---|
619 | $result =~ s/\s+$//; |
---|
620 | |
---|
621 | my(undef, $pids) = split(/\:/, $result); |
---|
622 | |
---|
623 | return $pids; |
---|
624 | } |
---|
625 | |
---|
626 | sub checkForSpace { |
---|
627 | my $fromDir = shift; |
---|
628 | my $toDir = shift; |
---|
629 | my $file = shift; |
---|
630 | my $mount; |
---|
631 | my $available; |
---|
632 | my $percentage; |
---|
633 | |
---|
634 | my(undef, undef, $available, $percentage) = getMountpoint($toDir); |
---|
635 | |
---|
636 | my $file_size = recordingSize($fromDir, $file); |
---|
637 | |
---|
638 | return $FALSE if (($available < $file_size) || ($percentage > 98)); |
---|
639 | return $TRUE; |
---|
640 | } |
---|
641 | |
---|
642 | sub getMountpoint { |
---|
643 | my $directory = shift; |
---|
644 | my $mp; |
---|
645 | my $size; |
---|
646 | my $free; |
---|
647 | my $percentage; |
---|
648 | |
---|
649 | my $buff = `df -kP $directory`; |
---|
650 | |
---|
651 | foreach my $line (split(/\n/, $buff)) { |
---|
652 | ($mp, $size, undef, $free, $percentage) = split(/\s+/, $line, 5) |
---|
653 | if ($line !~ /^Filesystem/); |
---|
654 | } |
---|
655 | |
---|
656 | $free = $free * 1024; |
---|
657 | $percentage =~ s/\%//; |
---|
658 | |
---|
659 | return($mp, $size, $free, $percentage); |
---|
660 | } |
---|
661 | |
---|
662 | sub recordingSize { |
---|
663 | my $directory = shift; |
---|
664 | my $recording = shift; |
---|
665 | |
---|
666 | if (!-e "$directory/$recording") { |
---|
667 | return -1; |
---|
668 | } |
---|
669 | |
---|
670 | my $size = -s "$directory/$recording"; |
---|
671 | |
---|
672 | return $size; |
---|
673 | } |
---|
674 | |
---|
675 | sub round { |
---|
676 | my $num = shift; |
---|
677 | my $place = shift; |
---|
678 | |
---|
679 | my($whole, $decimal) = split(/\./, $num); |
---|
680 | my $decimal_t = substr($decimal, 0, $place); |
---|
681 | |
---|
682 | if ($decimal_t) { |
---|
683 | my $rounder = substr($decimal, $place, 1); |
---|
684 | $decimal_t += 1 if ($rounder && $rounder > 5); |
---|
685 | } else { |
---|
686 | $decimal_t = 0; |
---|
687 | } |
---|
688 | |
---|
689 | return $whole.".".$decimal_t; |
---|
690 | } |
---|
691 | |
---|
692 | sub modesToString { |
---|
693 | my $modes = shift; |
---|
694 | |
---|
695 | if (!$modes) { |
---|
696 | return "\nNo mode specified, checking only.\n\n"; |
---|
697 | } |
---|
698 | |
---|
699 | my $string = "\nWorking in the following modes:\n"; |
---|
700 | |
---|
701 | if ($modes & $MODE_ARCHIVE) { |
---|
702 | $string .= "\t-> Archive Recordings\n"; |
---|
703 | } |
---|
704 | |
---|
705 | if ($modes & $MODE_ABANDONED) { |
---|
706 | $string .= "\t-> Delete abandoned recordings\n"; |
---|
707 | } |
---|
708 | |
---|
709 | if ($modes & $MODE_ZEROBYTE) { |
---|
710 | $string .= "\t-> Delete zero-byte recordings\n"; |
---|
711 | } |
---|
712 | |
---|
713 | if ($modes & $MODE_METADATA) { |
---|
714 | $string .= "\t-> Delete meta-data for missing recordings\n"; |
---|
715 | } |
---|
716 | |
---|
717 | if ($modes & $MODE_DELETED) { |
---|
718 | $string .= "\t-> Delete recordings in 'Deleted' group\n"; |
---|
719 | } |
---|
720 | |
---|
721 | $string .= "\n"; |
---|
722 | |
---|
723 | return $string; |
---|
724 | } |
---|
725 | |
---|
726 | sub doSql { |
---|
727 | my $sql = shift; |
---|
728 | |
---|
729 | if ($_DEBUG_) { |
---|
730 | print "DBG: $sql\n"; |
---|
731 | } else { |
---|
732 | my $sth = $DBH->prepare($sql); |
---|
733 | |
---|
734 | $sth->execute(); |
---|
735 | $sth->finish(); |
---|
736 | } |
---|
737 | } |
---|