vdr  1.7.31
videodir.c
Go to the documentation of this file.
1 /*
2  * videodir.c: Functions to maintain a distributed video directory
3  *
4  * See the main source file 'vdr.c' for copyright information and
5  * how to reach the author.
6  *
7  * $Id: videodir.c 2.4 2012/09/30 12:06:33 kls Exp $
8  */
9 
10 #include "videodir.h"
11 #include <ctype.h>
12 #include <errno.h>
13 #include <fcntl.h>
14 #include <stdio.h>
15 #include <stdlib.h>
16 #include <string.h>
17 #include <sys/stat.h>
18 #include <unistd.h>
19 #include "recording.h"
20 #include "tools.h"
21 
22 
23 //#define HARDLINK_TEST_ONLY
24 
25 const char *VideoDirectory = VIDEODIR;
26 
27 void SetVideoDirectory(const char *Directory)
28 {
29  VideoDirectory = strdup(Directory);
30 }
31 
33 private:
34  char *name, *stored, *adjusted;
36 public:
37  cVideoDirectory(void);
39  int FreeMB(int *UsedMB = NULL);
40  const char *Name(void) { return name ? name : VideoDirectory; }
41  const char *Stored(void) { return stored; }
42  int Length(void) { return length; }
43  bool IsDistributed(void) { return name != NULL; }
44  bool Next(void);
45  void Store(void);
46  const char *Adjust(const char *FileName);
47  };
48 
50 {
51  length = strlen(VideoDirectory);
52  name = (VideoDirectory[length - 1] == '0') ? strdup(VideoDirectory) : NULL;
53  stored = adjusted = NULL;
54  number = -1;
55  digits = 0;
56 }
57 
59 {
60  free(name);
61  free(stored);
62  free(adjusted);
63 }
64 
65 int cVideoDirectory::FreeMB(int *UsedMB)
66 {
67  return FreeDiskSpaceMB(name ? name : VideoDirectory, UsedMB);
68 }
69 
71 {
72  if (name) {
73  if (number < 0) {
74  int l = length;
75  while (l-- > 0 && isdigit(name[l]))
76  ;
77  l++;
78  digits = length - l;
79  int n = atoi(&name[l]);
80  if (n == 0)
81  number = n;
82  else
83  return false; // base video directory must end with zero
84  }
85  if (++number > 0) {
86  char buf[16];
87  if (sprintf(buf, "%0*d", digits, number) == digits) {
88  strcpy(&name[length - digits], buf);
89  return DirectoryOk(name);
90  }
91  }
92  }
93  return false;
94 }
95 
97 {
98  if (name) {
99  free(stored);
100  stored = strdup(name);
101  }
102 }
103 
104 const char *cVideoDirectory::Adjust(const char *FileName)
105 {
106  if (stored) {
107  free(adjusted);
108  adjusted = strdup(FileName);
109  return strncpy(adjusted, stored, length);
110  }
111  return NULL;
112 }
113 
114 cUnbufferedFile *OpenVideoFile(const char *FileName, int Flags)
115 {
116  const char *ActualFileName = FileName;
117 
118  // Incoming name must be in base video directory:
119  if (strstr(FileName, VideoDirectory) != FileName) {
120  esyslog("ERROR: %s not in %s", FileName, VideoDirectory);
121  errno = ENOENT; // must set 'errno' - any ideas for a better value?
122  return NULL;
123  }
124  // Are we going to create a new file?
125  if ((Flags & O_CREAT) != 0) {
126  cVideoDirectory Dir;
127  if (Dir.IsDistributed()) {
128  // Find the directory with the most free space:
129  int MaxFree = Dir.FreeMB();
130  while (Dir.Next()) {
131  int Free = FreeDiskSpaceMB(Dir.Name());
132  if (Free > MaxFree) {
133  Dir.Store();
134  MaxFree = Free;
135  }
136  }
137  if (Dir.Stored()) {
138  ActualFileName = Dir.Adjust(FileName);
139  if (!MakeDirs(ActualFileName, false))
140  return NULL; // errno has been set by MakeDirs()
141  if (symlink(ActualFileName, FileName) < 0) {
142  LOG_ERROR_STR(FileName);
143  return NULL;
144  }
145  ActualFileName = strdup(ActualFileName); // must survive Dir!
146  }
147  }
148  }
149  cUnbufferedFile *File = cUnbufferedFile::Create(ActualFileName, Flags, DEFFILEMODE);
150  if (ActualFileName != FileName)
151  free((char *)ActualFileName);
152  return File;
153 }
154 
156 {
157  int Result = File->Close();
158  delete File;
159  return Result;
160 }
161 
162 bool RenameVideoFile(const char *OldName, const char *NewName)
163 {
164  // Only the base video directory entry will be renamed, leaving the
165  // possible symlinks untouched. Going through all the symlinks and disks
166  // would be unnecessary work - maybe later...
167  if (rename(OldName, NewName) == -1) {
168  LOG_ERROR_STR(OldName);
169  return false;
170  }
171  return true;
172 }
173 
174 bool RemoveVideoFile(const char *FileName)
175 {
176  return RemoveFileOrDir(FileName, true);
177 }
178 
179 static bool StatNearestDir(const char *FileName, struct stat *Stat)
180 {
181  cString Name(FileName);
182  char *p;
183  while ((p = strrchr((char*)(const char*)Name + 1, '/')) != NULL) {
184  *p = 0; // truncate at last '/'
185  if (stat(Name, Stat) == 0) {
186  isyslog("StatNearestDir: Stating %s", (const char*)Name);
187  return true;
188  }
189  }
190  return false;
191 }
192 
193 bool HardLinkVideoFile(const char *OldName, const char *NewName)
194 {
195  // Incoming name must be in base video directory:
196  if (strstr(OldName, VideoDirectory) != OldName) {
197  esyslog("ERROR: %s not in %s", OldName, VideoDirectory);
198  return false;
199  }
200  if (strstr(NewName, VideoDirectory) != NewName) {
201  esyslog("ERROR: %s not in %s", NewName, VideoDirectory);
202  return false;
203  }
204 
205  const char *ActualNewName = NewName;
206  cString ActualOldName(ReadLink(OldName), true);
207 
208  // Some safety checks:
209  struct stat StatOldName;
210  if (lstat(ActualOldName, &StatOldName) == 0) {
211  if (S_ISLNK(StatOldName.st_mode)) {
212  esyslog("HardLinkVideoFile: Failed to resolve symbolic link %s", (const char*)ActualOldName);
213  return false;
214  }
215  }
216  else {
217  esyslog("HardLinkVideoFile: lstat failed on %s", (const char*)ActualOldName);
218  return false;
219  }
220  isyslog("HardLinkVideoFile: %s is on %i", (const char*)ActualOldName, (int)StatOldName.st_dev);
221 
222  // Find the video directory where ActualOldName is located
223 
224  cVideoDirectory Dir;
225  struct stat StatDir;
226  if (!StatNearestDir(NewName, &StatDir)) {
227  esyslog("HardLinkVideoFile: stat failed on %s", NewName);
228  return false;
229  }
230 
231  isyslog("HardLinkVideoFile: %s is on %i", NewName, (int)StatDir.st_dev);
232  if (StatDir.st_dev != StatOldName.st_dev) {
233  // Not yet found.
234 
235  if (!Dir.IsDistributed()) {
236  esyslog("HardLinkVideoFile: No matching video folder to hard link %s", (const char*)ActualOldName);
237  return false;
238  }
239 
240  // Search in video01 and upwards
241  bool found = false;
242  while (Dir.Next()) {
243  Dir.Store();
244  const char *TmpNewName = Dir.Adjust(NewName);
245  if (StatNearestDir(TmpNewName, &StatDir) && StatDir.st_dev == StatOldName.st_dev) {
246  isyslog("HardLinkVideoFile: %s is on %i (match)", TmpNewName, (int)StatDir.st_dev);
247  ActualNewName = TmpNewName;
248  found = true;
249  break;
250  }
251  isyslog("HardLinkVideoFile: %s is on %i", TmpNewName, (int)StatDir.st_dev);
252  }
253  if (ActualNewName == NewName) {
254  esyslog("HardLinkVideoFile: No matching video folder to hard link %s", (const char*)ActualOldName);
255  return false;
256  }
257 
258  // Looking good, we have a match. Create necessary folders.
259  if (!MakeDirs(ActualNewName, false))
260  return false;
261  // There's no guarantee that the directory of ActualNewName
262  // is on the same device as the dir that StatNearestDir found.
263  // But worst case is that the link fails.
264  }
265 
266 #ifdef HARDLINK_TEST_ONLY
267  // Do the hard link to *.vdr_ for testing only
268  char *name = NULL;
269  asprintf(&name, "%s_",ActualNewName);
270  link(ActualOldName, name);
271  free(name);
272  return false;
273 #endif // HARDLINK_TEST_ONLY
274 
275  // Try creating the hard link
276  if (link(ActualOldName, ActualNewName) != 0) {
277  // Failed to hard link. Maybe not allowed on file system.
278  LOG_ERROR_STR(ActualNewName);
279  isyslog("HardLinkVideoFile: failed to hard link from %s to %s", (const char*)ActualOldName, ActualNewName);
280  return false;
281  }
282 
283  if (ActualNewName != NewName) {
284  // video01 and up. Do the remaining symlink
285  if (symlink(ActualNewName, NewName) < 0) {
286  LOG_ERROR_STR(NewName);
287  return false;
288  }
289  }
290  return true;
291 }
292 
293 bool VideoFileSpaceAvailable(int SizeMB)
294 {
295  cVideoDirectory Dir;
296  if (Dir.IsDistributed()) {
297  if (Dir.FreeMB() >= SizeMB * 2) // base directory needs additional space
298  return true;
299  while (Dir.Next()) {
300  if (Dir.FreeMB() >= SizeMB)
301  return true;
302  }
303  return false;
304  }
305  return Dir.FreeMB() >= SizeMB;
306 }
307 
308 int VideoDiskSpace(int *FreeMB, int *UsedMB)
309 {
310  int free = 0, used = 0;
311  int deleted = DeletedRecordings.TotalFileSizeMB();
312  cVideoDirectory Dir;
313  do {
314  int u;
315  free += Dir.FreeMB(&u);
316  used += u;
317  } while (Dir.Next());
318  if (deleted > used)
319  deleted = used; // let's not get beyond 100%
320  free += deleted;
321  used -= deleted;
322  if (FreeMB)
323  *FreeMB = free;
324  if (UsedMB)
325  *UsedMB = used;
326  return (free + used) ? used * 100 / (free + used) : 0;
327 }
328 
329 cString PrefixVideoFileName(const char *FileName, char Prefix)
330 {
331  char PrefixedName[strlen(FileName) + 2];
332 
333  const char *p = FileName + strlen(FileName); // p points at the terminating 0
334  int n = 2;
335  while (p-- > FileName && n > 0) {
336  if (*p == '/') {
337  if (--n == 0) {
338  int l = p - FileName + 1;
339  strncpy(PrefixedName, FileName, l);
340  PrefixedName[l] = Prefix;
341  strcpy(PrefixedName + l + 1, p + 1);
342  return PrefixedName;
343  }
344  }
345  }
346  return NULL;
347 }
348 
349 cString NewVideoFileName(const char *FileName, const char *NewDirName)
350 {
351  char *NewDir = ExchangeChars(strdup(NewDirName), true);
352  if (NewDir) {
353  const char *p = FileName + strlen(FileName); // p points at the terminating 0
354  while (p-- > FileName) {
355  if (*p == '/')
356  break;
357  }
358  cString NewName = cString::sprintf("%s/%s%s", VideoDirectory, NewDir, p);
359  free(NewDir);
360  return NewName;
361  }
362  return NULL;
363 }
364 
365 void RemoveEmptyVideoDirectories(const char *IgnoreFiles[])
366 {
367  cVideoDirectory Dir;
368  do {
369  RemoveEmptyDirectories(Dir.Name(), false, IgnoreFiles);
370  } while (Dir.Next());
371 }
372 
373 bool IsOnVideoDirectoryFileSystem(const char *FileName)
374 {
375  cVideoDirectory Dir;
376  do {
377  if (EntriesOnSameFileSystem(Dir.Name(), FileName))
378  return true;
379  } while (Dir.Next());
380  return false;
381 }
382 
383 // --- cVideoDiskUsage -------------------------------------------------------
384 
385 #define DISKSPACECHEK 5 // seconds between disk space checks
386 #define MB_PER_MINUTE 25.75 // this is just an estimate!
387 
388 int cVideoDiskUsage::state = 0;
393 
395 {
396  if (time(NULL) - lastChecked > DISKSPACECHEK) {
397  int FreeMB;
398  int UsedPercent = VideoDiskSpace(&FreeMB);
399  if (FreeMB != freeMB) {
401  freeMB = FreeMB;
402  double MBperMinute = Recordings.MBperMinute();
403  if (MBperMinute <= 0)
404  MBperMinute = MB_PER_MINUTE;
405  freeMinutes = int(double(FreeMB) / MBperMinute);
406  state++;
407  }
408  lastChecked = time(NULL);
409  }
410  if (State != state) {
411  State = state;
412  return true;
413  }
414  return false;
415 }
416 
418 {
419  HasChanged(state);
420  return cString::sprintf("%s %d%% - %2d:%02d %s", tr("Disk"), usedPercent, freeMinutes / 60, freeMinutes % 60, tr("free"));
421 }