]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/cats/bvfs.c
Bvfs: Create cache tables when updating the cache if they don't exist
[bacula/bacula] / bacula / src / cats / bvfs.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2009-2009 Free Software Foundation Europe e.V.
5
6    The main author of Bacula is Kern Sibbald, with contributions from
7    many others, a complete list can be found in the file AUTHORS.
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version two of the GNU General Public
10    License as published by the Free Software Foundation, which is 
11    listed in the file LICENSE.
12
13    This program is distributed in the hope that it will be useful, but
14    WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16    General Public License for more details.
17
18    You should have received a copy of the GNU General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21    02110-1301, USA.
22
23    Bacula® is a registered trademark of Kern Sibbald.
24    The licensor of Bacula is the Free Software Foundation Europe
25    (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26    Switzerland, email:ftf@fsfeurope.org.
27 */
28
29 #define __SQL_C                       /* indicate that this is sql.c */
30
31 #include "bacula.h"
32 #include "cats/cats.h"
33 #include "lib/htable.h"
34 #include "bvfs.h"
35
36 #define dbglevel 10
37 #define dbglevel_sql 15
38
39 static int result_handler(void *ctx, int fields, char **row)
40 {
41    if (fields == 4) {
42       Pmsg4(0, "%s\t%s\t%s\t%s\n", 
43             row[0], row[1], row[2], row[3]);
44    } else if (fields == 5) {
45       Pmsg5(0, "%s\t%s\t%s\t%s\t%s\n", 
46             row[0], row[1], row[2], row[3], row[4]);
47    } else if (fields == 6) {
48       Pmsg6(0, "%s\t%s\t%s\t%s\t%s\t%s\n", 
49             row[0], row[1], row[2], row[3], row[4], row[5]);
50    } else if (fields == 7) {
51       Pmsg7(0, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", 
52             row[0], row[1], row[2], row[3], row[4], row[5], row[6]);
53    }
54    return 0;
55 }
56
57 Bvfs::Bvfs(JCR *j, B_DB *mdb) {
58    jcr = j;
59    jcr->inc_use_count();
60    db = mdb;                 /* need to inc ref count */
61    jobids = get_pool_memory(PM_NAME);
62    pattern = get_pool_memory(PM_NAME);
63    *pattern = *jobids = 0;
64    dir_filenameid = pwd_id = offset = 0;
65    see_copies = see_all_version = false;
66    limit = 1000;
67    attr = new_attr(jcr);
68    list_entries = result_handler;
69    user_data = this;
70 }
71
72 Bvfs::~Bvfs() {
73    free_pool_memory(jobids);
74    free_pool_memory(pattern);
75    free_attr(attr);
76    jcr->dec_use_count();
77 }
78
79 /* 
80  * TODO: Find a way to let the user choose how he wants to display
81  * files and directories
82  */
83
84
85 /* 
86  * Working Object to store PathId already seen (avoid
87  * database queries), equivalent to %cache_ppathid in perl
88  */
89
90 #define NITEMS 50000
91 class pathid_cache {
92 private:
93    hlink *nodes;
94    int nb_node;
95    int max_node;
96    htable *cache_ppathid;
97
98 public:
99    pathid_cache() {
100       hlink link;
101       cache_ppathid = (htable *)malloc(sizeof(htable));
102       cache_ppathid->init(&link, &link, NITEMS);
103       max_node = NITEMS;
104       nodes = (hlink *) malloc(max_node * sizeof (hlink));
105       nb_node = 0;
106    }
107
108    hlink *get_hlink() {
109       if (nb_node >= max_node) {
110          max_node *= 2;
111          nodes = (hlink *)brealloc(nodes, sizeof(hlink) * max_node);
112       }
113       return nodes + nb_node++;
114    }
115
116    bool lookup(char *pathid) {
117       bool ret = cache_ppathid->lookup(pathid) != NULL;
118       return ret;
119    }
120    
121    void insert(char *pathid) {
122       hlink *h = get_hlink();
123       cache_ppathid->insert(pathid, h);
124    }
125
126    ~pathid_cache() {
127       cache_ppathid->destroy();
128       free(cache_ppathid);
129       free(nodes);
130    }
131 } ;
132
133 /* Return the parent_dir with the trailing /  (update the given string)
134  * TODO: see in the rest of bacula if we don't have already this function
135  * dir=/tmp/toto/
136  * dir=/tmp/
137  * dir=/
138  * dir=
139  */
140 char *bvfs_parent_dir(char *path)
141 {
142    char *p = path;
143    int len = strlen(path) - 1;
144
145    if (len >= 0 && path[len] == '/') {      /* if directory, skip last / */
146       path[len] = '\0';
147    }
148
149    if (len > 0) {
150       p += len;
151       while (p > path && !IsPathSeparator(*p)) {
152          p--;
153       }
154       p[1] = '\0';
155    }
156    return path;
157 }
158
159 /* Return the basename of the with the trailing /
160  * TODO: see in the rest of bacula if we don't have
161  * this function already
162  */
163 char *bvfs_basename_dir(char *path)
164 {
165    char *p = path;
166    int len = strlen(path) - 1;
167
168    if (path[len] == '/') {      /* if directory, skip last / */
169       len -= 1;
170    }
171
172    if (len > 0) {
173       p += len;
174       while (p > path && !IsPathSeparator(*p)) {
175          p--;
176       }
177       p = p+1;                  /* skip first / */
178    } 
179    return p;
180 }
181
182 static void build_path_hierarchy(JCR *jcr, B_DB *mdb, 
183                                  pathid_cache &ppathid_cache, 
184                                  char *org_pathid, char *path)
185 {
186    Dmsg1(dbglevel, "build_path_hierarchy(%s)\n", path);
187    char pathid[50];
188    ATTR_DBR parent;
189    char *bkp = mdb->path;
190    strncpy(pathid, org_pathid, sizeof(pathid));
191
192    /* Does the ppathid exist for this ? we use a memory cache...  In order to
193     * avoid the full loop, we consider that if a dir is allready in the
194     * brestore_pathhierarchy table, then there is no need to calculate all the
195     * hierarchy
196     */
197    while (path && *path)
198    {
199       if (!ppathid_cache.lookup(pathid))
200       {
201          Mmsg(mdb->cmd, 
202               "SELECT PPathId FROM brestore_pathhierarchy WHERE PathId = %s",
203               pathid);
204
205          QUERY_DB(jcr, mdb, mdb->cmd);
206          /* Do we have a result ? */
207          if (sql_num_rows(mdb) > 0) {
208             ppathid_cache.insert(pathid);
209             /* This dir was in the db ...
210              * It means we can leave, the tree has allready been built for
211              * this dir
212              */
213             goto bail_out;
214          } else {
215             /* search or create parent PathId in Path table */
216             mdb->path = bvfs_parent_dir(path);
217             mdb->pnl = strlen(mdb->path);
218             if (!db_create_path_record(jcr, mdb, &parent)) {
219                goto bail_out;
220             }
221             ppathid_cache.insert(pathid);
222             
223             Mmsg(mdb->cmd,
224                  "INSERT INTO brestore_pathhierarchy (PathId, PPathId) "
225                  "VALUES (%s,%lld)",
226                  pathid, (uint64_t) parent.PathId);
227             
228             INSERT_DB(jcr, mdb, mdb->cmd);
229
230             edit_uint64(parent.PathId, pathid);
231             path = mdb->path;   /* already done */
232          }
233       } else {
234          /* It's allready in the cache.  We can leave, no time to waste here,
235           * all the parent dirs have allready been done
236           */
237          goto bail_out;
238       }
239    }   
240
241 bail_out:
242    mdb->path = bkp;
243    mdb->fnl = 0;
244 }
245
246 /* 
247  * Internal function to update path_hierarchy cache with a shared pathid cache
248  */
249 static void update_path_hierarchy_cache(JCR *jcr,
250                                         B_DB *mdb,
251                                         pathid_cache &ppathid_cache,
252                                         JobId_t JobId)
253 {
254    Dmsg0(dbglevel, "update_path_hierarchy_cache()\n");
255
256    uint32_t num;
257    char jobid[50];
258    edit_uint64(JobId, jobid);
259  
260    db_lock(mdb);
261    db_start_transaction(jcr, mdb);
262
263    Mmsg(mdb->cmd, "SELECT 1 FROM brestore_knownjobid WHERE JobId = %s", jobid);
264    
265    if (!QUERY_DB(jcr, mdb, mdb->cmd) || sql_num_rows(mdb) > 0) {
266       Dmsg1(dbglevel, "already computed %d\n", (uint32_t)JobId );
267       goto bail_out;
268    }
269
270    /* Inserting path records for JobId */
271    Mmsg(mdb->cmd, "INSERT INTO brestore_pathvisibility (PathId, JobId) "
272                   "SELECT DISTINCT PathId, JobId FROM File WHERE JobId = %s",
273         jobid);
274    QUERY_DB(jcr, mdb, mdb->cmd);
275
276
277    /* Now we have to do the directory recursion stuff to determine missing
278     * visibility We try to avoid recursion, to be as fast as possible We also
279     * only work on not allready hierarchised directories...
280     */
281    Mmsg(mdb->cmd, 
282      "SELECT brestore_pathvisibility.PathId, Path "
283        "FROM brestore_pathvisibility "
284             "JOIN Path ON( brestore_pathvisibility.PathId = Path.PathId) "
285             "LEFT JOIN brestore_pathhierarchy "
286          "ON (brestore_pathvisibility.PathId = brestore_pathhierarchy.PathId) "
287       "WHERE brestore_pathvisibility.JobId = %s "
288         "AND brestore_pathhierarchy.PathId IS NULL "
289       "ORDER BY Path", jobid);
290    Dmsg1(dbglevel_sql, "q=%s\n", mdb->cmd);
291    QUERY_DB(jcr, mdb, mdb->cmd);
292
293    /* TODO: I need to reuse the DB connection without emptying the result 
294     * So, now i'm copying the result in memory to be able to query the
295     * catalog descriptor again.
296     */
297    num = sql_num_rows(mdb);
298    if (num > 0) {
299       char **result = (char **)malloc (num * 2 * sizeof(char *));
300       
301       SQL_ROW row;
302       int i=0;
303       while((row = sql_fetch_row(mdb))) {
304          result[i++] = bstrdup(row[0]);
305          result[i++] = bstrdup(row[1]);
306       }
307       
308       i=0;
309       while (num > 0) {
310          build_path_hierarchy(jcr, mdb, ppathid_cache, result[i], result[i+1]);
311          free(result[i++]);
312          free(result[i++]);
313          num--;
314       }
315       free(result);
316    }
317    
318    Mmsg(mdb->cmd, 
319   "INSERT INTO brestore_pathvisibility (PathId, JobId)  "
320    "SELECT a.PathId,%s "
321    "FROM ( "
322      "SELECT DISTINCT h.PPathId AS PathId "
323        "FROM brestore_pathhierarchy AS h "
324        "JOIN  brestore_pathvisibility AS p ON (h.PathId=p.PathId) "
325       "WHERE p.JobId=%s) AS a LEFT JOIN "
326        "(SELECT PathId "
327           "FROM brestore_pathvisibility "
328          "WHERE JobId=%s) AS b ON (a.PathId = b.PathId) "
329    "WHERE b.PathId IS NULL",  jobid, jobid, jobid);
330
331    do {
332       QUERY_DB(jcr, mdb, mdb->cmd);
333    } while (sql_affected_rows(mdb) > 0);
334    
335    Mmsg(mdb->cmd, "INSERT INTO brestore_knownjobid (JobId) VALUES (%s)", jobid);
336    INSERT_DB(jcr, mdb, mdb->cmd);
337
338 bail_out:
339    db_end_transaction(jcr, mdb);
340    db_unlock(mdb);
341 }
342
343 /* 
344  * Find an store the filename descriptor for empty directories Filename.Name=''
345  */
346 DBId_t Bvfs::get_dir_filenameid()
347 {
348    uint32_t id;
349    if (dir_filenameid) {
350       return dir_filenameid;
351    }
352    POOL_MEM q;
353    Mmsg(q, "SELECT FilenameId FROM Filename WHERE Name = ''");
354    db_sql_query(db, q.c_str(), db_int_handler, &id);
355    dir_filenameid = id;
356    return dir_filenameid;
357 }
358
359 void bvfs_update_cache(JCR *jcr, B_DB *mdb)
360 {
361    uint32_t nb=0;
362    db_lock(mdb);
363    db_start_transaction(jcr, mdb);
364
365    Mmsg(mdb->cmd, "SELECT 1 from brestore_knownjobid LIMIT 1");
366    /* TODO: Add this code in the make_bacula_table script */
367    if (!QUERY_DB(jcr, mdb, mdb->cmd)) {
368       Dmsg0(dbglevel, "Creating cache table\n");
369       Mmsg(mdb->cmd,
370            "CREATE TABLE brestore_knownjobid ("
371            "JobId integer NOT NULL, "
372            "CONSTRAINT brestore_knownjobid_pkey PRIMARY KEY (JobId))");
373       QUERY_DB(jcr, mdb, mdb->cmd);
374
375       Mmsg(mdb->cmd,
376            "CREATE TABLE brestore_pathhierarchy ( "
377            "PathId integer NOT NULL, "
378            "PPathId integer NOT NULL, "
379            "CONSTRAINT brestore_pathhierarchy_pkey "
380            "PRIMARY KEY (PathId))");
381       QUERY_DB(jcr, mdb, mdb->cmd); 
382
383       Mmsg(mdb->cmd,
384            "CREATE INDEX brestore_pathhierarchy_ppathid "
385            "ON brestore_pathhierarchy (PPathId)");
386       QUERY_DB(jcr, mdb, mdb->cmd);
387
388       Mmsg(mdb->cmd, 
389            "CREATE TABLE brestore_pathvisibility ("
390            "PathId integer NOT NULL, "
391            "JobId integer NOT NULL, "
392            "Size int8 DEFAULT 0, "
393            "Files int4 DEFAULT 0, "
394            "CONSTRAINT brestore_pathvisibility_pkey "
395            "PRIMARY KEY (JobId, PathId))");
396       QUERY_DB(jcr, mdb, mdb->cmd);
397
398       Mmsg(mdb->cmd, 
399            "CREATE INDEX brestore_pathvisibility_jobid "
400            "ON brestore_pathvisibility (JobId)");
401       QUERY_DB(jcr, mdb, mdb->cmd);
402
403    }
404
405    POOLMEM *jobids = get_pool_memory(PM_NAME);
406    *jobids = 0;
407
408    Mmsg(mdb->cmd, 
409  "SELECT JobId from Job "
410   "WHERE JobId NOT IN (SELECT JobId FROM brestore_knownjobid) "
411     "AND Type IN ('B') AND JobStatus IN ('T', 'f', 'A') "
412   "ORDER BY JobId");
413
414    db_sql_query(mdb, mdb->cmd, db_get_int_handler, jobids);
415
416    bvfs_update_path_hierarchy_cache(jcr, mdb, jobids);
417
418    db_end_transaction(jcr, mdb);
419    db_start_transaction(jcr, mdb);
420    Dmsg0(dbglevel, "Cleaning pathvisibility\n");
421    Mmsg(mdb->cmd, 
422         "DELETE FROM brestore_pathvisibility "
423          "WHERE NOT EXISTS "
424         "(SELECT 1 FROM Job WHERE JobId=brestore_pathvisibility.JobId)");
425    nb = DELETE_DB(jcr, mdb, mdb->cmd);
426    Dmsg1(dbglevel, "Affected row(s) = %d\n", nb);
427
428    Dmsg0(dbglevel, "Cleaning knownjobid\n");
429    Mmsg(mdb->cmd,         
430         "DELETE FROM brestore_knownjobid "
431          "WHERE NOT EXISTS "
432         "(SELECT 1 FROM Job WHERE JobId=brestore_knownjobid.JobId)");
433    nb = DELETE_DB(jcr, mdb, mdb->cmd);
434    Dmsg1(dbglevel, "Affected row(s) = %d\n", nb);
435
436    db_end_transaction(jcr, mdb);
437 }
438
439 /*
440  * Update the bvfs cache for given jobids (1,2,3,4)
441  */
442 void
443 bvfs_update_path_hierarchy_cache(JCR *jcr, B_DB *mdb, char *jobids)
444 {
445    pathid_cache ppathid_cache;
446    JobId_t JobId;
447    char *p;
448
449    for (p=jobids; ; ) {
450       int stat = get_next_jobid_from_list(&p, &JobId);
451       if (stat < 0) {
452          return;
453       }
454       if (stat == 0) {
455          break;
456       }
457       Dmsg1(dbglevel, "Updating cache for %lld\n", (uint64_t) JobId);
458       update_path_hierarchy_cache(jcr, mdb, ppathid_cache, JobId);
459    }
460 }
461
462 /* 
463  * Update the bvfs cache for current jobids
464  */
465 void Bvfs::update_cache()
466 {
467    bvfs_update_path_hierarchy_cache(jcr, db, jobids);
468 }
469
470 /* Change the current directory, returns true if the path exists */
471 bool Bvfs::ch_dir(char *path)
472 {
473    pm_strcpy(db->path, path);
474    db->pnl = strlen(db->path);
475    pwd_id = db_get_path_record(jcr, db); 
476    return pwd_id != 0;
477 }
478
479 /* 
480  * Get all file versions for a specified client
481  */
482 void Bvfs::get_all_file_versions(DBId_t pathid, DBId_t fnid, char *client)
483 {
484    Dmsg3(dbglevel, "get_all_file_versions(%lld, %lld, %s)\n", (uint64_t)pathid,
485          (uint64_t)fnid, client);
486    char ed1[50], ed2[50];
487    POOL_MEM q;
488    if (see_copies) {
489       Mmsg(q, " AND Job.Type IN ('C', 'B') ");
490    } else {
491       Mmsg(q, " AND Job.Type = 'B' ");
492    }
493
494    POOL_MEM query;
495
496    Mmsg(query, 
497 "SELECT File.JobId, File.FileId, File.LStat, "
498        "File.Md5, Media.VolumeName, Media.InChanger "
499 "FROM File, Job, Client, JobMedia, Media "
500 "WHERE File.FilenameId = %s "
501   "AND File.PathId=%s "
502   "AND File.JobId = Job.JobId "
503   "AND Job.ClientId = Client.ClientId "
504   "AND Job.JobId = JobMedia.JobId "
505   "AND File.FileIndex >= JobMedia.FirstIndex "
506   "AND File.FileIndex <= JobMedia.LastIndex "
507   "AND JobMedia.MediaId = Media.MediaId "
508   "AND Client.Name = '%s' "
509   "%s ORDER BY FileId LIMIT %d OFFSET %d"
510         ,edit_uint64(fnid, ed1), edit_uint64(pathid, ed2), client, q.c_str(),
511         limit, offset);
512
513    db_sql_query(db, query.c_str(), list_entries, user_data);
514 }
515
516 DBId_t Bvfs::get_root()
517 {
518    *db->path = 0;
519    return db_get_path_record(jcr, db);
520 }
521
522 /* 
523  * Retrieve . and .. information
524  */
525 void Bvfs::ls_special_dirs()
526 {
527    Dmsg1(dbglevel, "ls_special_dirs(%lld)\n", (uint64_t)pwd_id);
528    char ed1[50], ed2[50];
529    if (!*jobids) {
530       return;
531    }
532    if (!dir_filenameid) {
533       get_dir_filenameid();
534    }
535
536    POOL_MEM query;
537    Mmsg(query, 
538 "((SELECT PPathId AS PathId, '..' AS Path "
539     "FROM  brestore_pathhierarchy "
540    "WHERE  PathId = %s) "
541 "UNION "
542  "(SELECT %s AS PathId, '.' AS Path))",
543         edit_uint64(pwd_id, ed1), ed1);
544
545    POOL_MEM query2;
546    Mmsg(query2, 
547 "SELECT tmp.PathId, tmp.Path, JobId, LStat "
548   "FROM %s AS tmp  LEFT JOIN ( " // get attributes if any
549        "SELECT File1.PathId AS PathId, File1.JobId AS JobId, "
550               "File1.LStat AS LStat FROM File AS File1 "
551        "WHERE File1.FilenameId = %s "
552        "AND File1.JobId IN (%s)) AS listfile1 "
553   "ON (tmp.PathId = listfile1.PathId) "
554   "ORDER BY tmp.Path, JobId DESC ",
555         query.c_str(), edit_uint64(dir_filenameid, ed2), jobids);
556
557    Dmsg1(dbglevel_sql, "q=%s\n", query.c_str());
558    db_sql_query(db, query2.c_str(), list_entries, user_data);
559 }
560
561 void Bvfs::ls_dirs()
562 {
563    Dmsg1(dbglevel, "ls_dirs(%lld)\n", (uint64_t)pwd_id);
564    char ed1[50], ed2[50];
565    if (!*jobids) {
566       return;
567    }
568
569    POOL_MEM filter;
570    if (*pattern) {
571       Mmsg(filter, " AND Path2.Path %s '%s' ", SQL_MATCH, pattern);
572    }
573
574    if (!dir_filenameid) {
575       get_dir_filenameid();
576    }
577
578    /* Let's retrieve the list of the visible dirs in this dir ...
579     * First, I need the empty filenameid to locate efficiently
580     * the dirs in the file table
581     * my $dir_filenameid = $self->get_dir_filenameid();
582     */
583    /* Then we get all the dir entries from File ... */
584    POOL_MEM query;
585    Mmsg(query,
586 //        0     1      2      3
587 "SELECT PathId, Path, JobId, LStat FROM ( "
588     "SELECT Path1.PathId AS PathId, Path1.Path AS Path, "
589            "lower(Path1.Path) AS lpath, "
590            "listfile1.JobId AS JobId, listfile1.LStat AS LStat "
591     "FROM ( "
592       "SELECT DISTINCT brestore_pathhierarchy1.PathId AS PathId "
593       "FROM brestore_pathhierarchy AS brestore_pathhierarchy1 "
594       "JOIN Path AS Path2 "
595         "ON (brestore_pathhierarchy1.PathId = Path2.PathId) "
596       "JOIN brestore_pathvisibility AS brestore_pathvisibility1 "
597         "ON (brestore_pathhierarchy1.PathId = brestore_pathvisibility1.PathId) "
598       "WHERE brestore_pathhierarchy1.PPathId = %s "
599       "AND brestore_pathvisibility1.jobid IN (%s) "
600            "%s "
601      ") AS listpath1 "
602    "JOIN Path AS Path1 ON (listpath1.PathId = Path1.PathId) "
603
604    "LEFT JOIN ( " /* get attributes if any */
605        "SELECT File1.PathId AS PathId, File1.JobId AS JobId, "
606               "File1.LStat AS LStat FROM File AS File1 "
607        "WHERE File1.FilenameId = %s "
608        "AND File1.JobId IN (%s)) AS listfile1 "
609        "ON (listpath1.PathId = listfile1.PathId) "
610     ") AS A ORDER BY 2,3 DESC LIMIT %d OFFSET %d",
611         edit_uint64(pwd_id, ed1),
612         jobids,
613         filter.c_str(),
614         edit_uint64(dir_filenameid, ed2),
615         jobids,
616         limit, offset);
617
618    Dmsg1(dbglevel_sql, "q=%s\n", query.c_str());
619    db_sql_query(db, query.c_str(), list_entries, user_data);
620 }
621
622 void Bvfs::ls_files()
623 {
624    Dmsg1(dbglevel, "ls_files(%lld)\n", (uint64_t)pwd_id);
625    char ed1[50];
626    if (!*jobids) {
627       return ;
628    }
629
630    if (!pwd_id) {
631       ch_dir(get_root());
632    }
633
634    POOL_MEM filter;
635    if (*pattern) {
636       Mmsg(filter, " AND Filename.Name %s '%s' ", SQL_MATCH, pattern);
637    }
638
639    POOL_MEM query;
640    Mmsg(query, // 0         1              2             3          4
641 "SELECT File.FilenameId, listfiles.Name, File.JobId, File.LStat, listfiles.id "
642 "FROM File, ( "
643        "SELECT Filename.Name as Name, max(File.FileId) as id "
644          "FROM File, Filename "
645         "WHERE File.FilenameId = Filename.FilenameId "
646           "AND Filename.Name != '' "
647           "AND File.PathId = %s "
648           "AND File.JobId IN (%s) "
649           "%s "
650         "GROUP BY Filename.Name "
651         "ORDER BY Filename.Name LIMIT %d OFFSET %d "
652      ") AS listfiles "
653 "WHERE File.FileId = listfiles.id",
654         edit_uint64(pwd_id, ed1),
655         jobids,
656         filter.c_str(),
657         limit,
658         offset);
659    Dmsg1(dbglevel_sql, "q=%s\n", query.c_str());
660    db_sql_query(db, query.c_str(), list_entries, user_data);
661 }