rda-python-dsarch 3.0.11__tar.gz → 3.0.13__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {rda_python_dsarch-3.0.11/src/rda_python_dsarch.egg-info → rda_python_dsarch-3.0.13}/PKG-INFO +1 -1
  2. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/pyproject.toml +1 -1
  3. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch/dsarch.py +5 -4
  4. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch/dsarch.usg +40 -6
  5. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch/pg_arch.py +232 -8
  6. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13/src/rda_python_dsarch.egg-info}/PKG-INFO +1 -1
  7. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/LICENSE +0 -0
  8. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/MANIFEST.in +0 -0
  9. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/README.md +0 -0
  10. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/setup.cfg +0 -0
  11. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch/PgArch.py +0 -0
  12. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch/PgMeta.py +0 -0
  13. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch/__init__.py +0 -0
  14. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch/ds_arch.py +0 -0
  15. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch/pg_meta.py +0 -0
  16. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch.egg-info/SOURCES.txt +0 -0
  17. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch.egg-info/dependency_links.txt +0 -0
  18. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch.egg-info/entry_points.txt +0 -0
  19. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch.egg-info/requires.txt +0 -0
  20. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/src/rda_python_dsarch.egg-info/top_level.txt +0 -0
  21. {rda_python_dsarch-3.0.11 → rda_python_dsarch-3.0.13}/tests/test_dsarch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsarch
3
- Version: 3.0.11
3
+ Version: 3.0.13
4
4
  Summary: RDA Python package for utility program dsarch archive RDA data
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-dsarch
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rda_python_dsarch"
7
- version = "3.0.11"
7
+ version = "3.0.13"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -1802,7 +1802,8 @@ class DsArch(PgArch, PgMeta):
1802
1802
  oarch = 0
1803
1803
  ofile = (self.join_paths(dsid, sfile) if oarch else None)
1804
1804
  if sarch:
1805
- tardir = "{}/{}".format(self.PGLOG['DECSHOME'], dsid)
1805
+ # saved files are tarred relative to DECSHOME/<dsid>/<type>, per tar_backup_savedfiles()
1806
+ tardir = "{}/{}/{}".format(self.PGLOG['DECSHOME'], dsid, type)
1806
1807
  else:
1807
1808
  tardir = "{}/{}/{}".format(self.PGLOG['DECSHOME'], self.PGLOG['BACKUPEP'], dsid)
1808
1809
  afile = "{}/{}".format(tardir, sfile)
@@ -2048,7 +2049,7 @@ class DsArch(PgArch, PgMeta):
2048
2049
  self.print_row_format(pgrec, fnames, hash)
2049
2050
  self.view_keyvalues(dsid, kvalues, getkeys)
2050
2051
  if fnames and 'PE' in self.params: self.get_period_info()
2051
- if 'WN' in self.params: self.view_filenumber(dsid, 0)
2052
+ if 'WN' in self.params or 'WS' in self.params: self.view_filenumber(dsid, 0)
2052
2053
  return 1 # get one dataset record
2053
2054
 
2054
2055
  def get_period_info(self):
@@ -2205,7 +2206,7 @@ class DsArch(PgArch, PgMeta):
2205
2206
  onames = self.params['ON'] if 'ON' in self.params else "I"
2206
2207
  qnames = fnames
2207
2208
  if fnames.find('Y') > -1 and fnames.find('X') < 0: qnames += 'X'
2208
- if 'WN' in self.params and fnames.find('I') < 0: qnames += 'I'
2209
+ if ('WN' in self.params or 'WS' in self.params) and fnames.find('I') < 0: qnames += 'I'
2209
2210
  qnames += self.append_order_fields(onames, qnames, tname, "Y")
2210
2211
  condition = self.get_condition(tname)
2211
2212
  if 'ON' in self.params and ('OB' in self.params or re.search(r'Y', onames, re.I)):
@@ -2226,7 +2227,7 @@ class DsArch(PgArch, PgMeta):
2226
2227
  cnt = self.print_column_format(pgrecs, fnames, hash, lens)
2227
2228
  s = 's' if cnt > 1 else ''
2228
2229
  self.pglog("{} group record{} retrieved".format(cnt, s), self.LOGWRN)
2229
- if 'WN' in self.params: self.view_filenumber(dsid, pgrecs['gindex'], cnt)
2230
+ if 'WN' in self.params or 'WS' in self.params: self.view_filenumber(dsid, pgrecs['gindex'], cnt)
2230
2231
  else:
2231
2232
  self.pglog("No group found for " + condition, self.LOGWRN)
2232
2233
 
@@ -609,10 +609,11 @@ QSCAT observations.<:>
609
609
  Results can be filtered by group index (-GI), name (-GN), or parent
610
610
  index (-PI). The -GN option accepts the '%' wildcard.
611
611
 
612
- For example, to retrieve the default group fields and file counts for
613
- groups 1 and 2 of d260001:
612
+ For example, to retrieve the default group fields, the file numbers
613
+ and sizes, and the storage and Quasar backup checks for groups 1 and
614
+ 2 of d260001:
614
615
 
615
- dsarch d260001 GG -WN -GI 1 2
616
+ dsarch d260001 GG -WN -WS -GI 1 2
616
617
 
617
618
  <<Content of the output>>
618
619
  Dataset<=>d260001
@@ -621,7 +622,13 @@ GroupIndex<:>GroupName<:>ParentIndex<:>Title<:>GroupPattern<:>BackupFlag<:>Saved
621
622
  2<:>MONTHLY<:>0<:>MONTHLY OAFlux Products<:><:>P<:><:>monthly<:>
622
623
  2 group records retrieved
623
624
  d260001-G1: DS-11094192261 DC-327 WC-327 SS-8909173908 SC-51
625
+ d260001-G1 Web Files: Total 327(11094192261), Missing 0(0), Quasar Backup&DR 327(11094192261)
626
+ d260001-G1 Saved Files: Total 51(8909173908), Missing 0(0), Quasar Backup&DR 51(8909173908)
624
627
  d260001-G2: DS-552780725 DC-522 WC-522 SS-465789806 SC-51
628
+ d260001-G2 Web Files: Total 522(552780725), Missing 2(2116424), Missing but on Quasar 1(1058212), Missing on Glade 2(2116424), Quasar Backup&DR 521(551722513), Not Backed Up 1(1058212)
629
+ d260001-G2-W-D: monthly/2025/oaflux_202511.nc: Missing on Glade; on Quasar Backup&DR (Backup ID 3456789)
630
+ d260001-G2-W-D: monthly/2025/oaflux_202512.nc: Missing on Glade; Not Backed Up
631
+ d260001-G2 Saved Files: Total 51(465789806), Missing 0(0), Quasar Backup&DR 51(465789806)
625
632
 
626
633
 
627
634
  3.3.3 Delete Group Information
@@ -1909,12 +1916,39 @@ behavior.
1909
1916
 
1910
1917
  -WN or -WithFileNumber, recomputes and updates Saved/Web file
1911
1918
  counts for the dataset and any specified groups. With -GD
1912
- (-GetDataset), the counts are calculated on the fly and included
1913
- in the output.
1919
+ (-GetDataset) or -GG (-GetGroup), shows the Web/Saved file numbers
1920
+ of the dataset or each group, and checks their files as described
1921
+ below.
1914
1922
 
1915
1923
  -WS or -WithFileSize, rechecks Web file sizes and timestamps for
1916
1924
  provided web files with -WF (-WebFile), and updates them into GDEXDB
1917
- This Mode option works with action -SW (SetWebFile) only.
1925
+ with action -SW (SetWebFile). With -GD (-GetDataset) or -GG
1926
+ (-GetGroup), shows the Web/Saved file sizes of the dataset or each
1927
+ group, and checks their files as described below.
1928
+
1929
+ With -GD/-GG, either -WN or -WS also checks every Web and Saved file
1930
+ (including the files of sub groups for a group) that is not deleted:
1931
+ - a file with Location Flag G or B must exist on Glade, and one
1932
+ with Location Flag O or B on Boreas (object store);
1933
+ - a Saved file on Glade (Location Flag G or B) is looked for in
1934
+ COLD_STORAGE, <decsdata>/COLD_STORAGE/<dsid> or
1935
+ <decsdata>/cold_storage_<YYYYMMDD>/COLD_STORAGE/<dsid>, and is
1936
+ counted as In COLD_STORAGE whether or not it is on Glade too;
1937
+ - its Quasar backup, by the backup file (bfile) of its Backup ID:
1938
+ type B for Quasar Backup, type D for Quasar Backup & Disaster
1939
+ Recovery, counted once the backup file status is A; Pending
1940
+ when it is not, and Not Backed Up when it has no Backup ID.
1941
+ Files with Location Flag C (CGD) or R (remote) are not stored by
1942
+ GDEX and are not checked. One summary line per file category shows
1943
+ the file counts with -WN and the total file sizes with -WS (both as
1944
+ Count(Size) when both options are given): the Total, the total
1945
+ Missing (each file counted once; a Saved file found in COLD_STORAGE
1946
+ is not missing), Missing but on Quasar (missing files that can still
1947
+ be restored from a finished Quasar backup with action -RQ, so Missing
1948
+ minus it is what is lost), then the result of each check. It is
1949
+ followed by one line for every file missing or not backed up yet;
1950
+ the line of a missing file also tells its Quasar backup, or that it
1951
+ is not backed up.
1918
1952
 
1919
1953
  -XC or -CrossCopy, used with -AW or -AS to fill gaps in storage:
1920
1954
  copies files from Glade disk to bOreas (or vice versa) wherever
@@ -21,6 +21,7 @@ and caching dataset/group metadata.
21
21
  import time
22
22
  import re
23
23
  import os
24
+ import glob
24
25
  from os import path as op
25
26
  from rda_python_common.pg_split import PgSplit
26
27
  from rda_python_common.pg_cmd import PgCMD
@@ -61,6 +62,8 @@ class PgArch(PgOPT, PgCMD, PgSplit):
61
62
  'SP' : '', # dataset savedpath
62
63
  }
63
64
  self.webpaths = {}
65
+ self.OBJKEYS = {} # object keys listed per bucket:dsid, by object_file_exists()
66
+ self.COLDDIRS = {} # cold storage directories found per dsid, by cold_storage_file()
64
67
  self.savedpaths = {}
65
68
  self.grouptypes = {}
66
69
  self.CORDERS = {}
@@ -1417,27 +1420,248 @@ class PgArch(PgOPT, PgCMD, PgSplit):
1417
1420
  else:
1418
1421
  cnt = 1
1419
1422
  grecs = [gindex]
1423
+ # -WN shows the file numbers and -WS the file sizes; either one also checks the
1424
+ # files on storage and on Quasar, see check_file_storage()
1425
+ wn = 'WN' in self.params
1426
+ ws = 'WS' in self.params
1420
1427
  fields = ("dwebcnt, webcnt, cdwcnt, nwebcnt, dweb_size, nweb_size, savedcnt, saved_size")
1428
+ dcnd = "dsid = '{}'".format(dsid)
1421
1429
  for i in range(cnt):
1422
1430
  if grecs[i] == 0:
1423
1431
  table = "dataset"
1424
- cnd = "dsid = '{}'".format(dsid)
1432
+ cnd = dcnd
1425
1433
  head = dsid
1426
1434
  else:
1427
1435
  table = "dsgroup"
1428
- cnd = "dsid = '{}' AND gindex = {}".format(dsid, grecs[i])
1436
+ cnd = "{} AND gindex = {}".format(dcnd, grecs[i])
1429
1437
  head = "{}-G{}".format(dsid, grecs[i])
1430
1438
  pgrec = self.pgget(table, fields, cnd, self.PGOPT['extlog'])
1431
1439
  if pgrec:
1432
1440
  msg = ''
1433
1441
  if pgrec['webcnt']:
1434
- if pgrec['dwebcnt']: msg += " DS-{} DC-{}".format(pgrec['dweb_size'], pgrec['dwebcnt'])
1435
- if pgrec['cdwcnt']: msg += " CDC-{}".format(pgrec['cdwcnt'])
1436
- if pgrec['nwebcnt']: msg += " NS-{} NC-{}".format(pgrec['nweb_size'], pgrec['nwebcnt'])
1437
- msg += " WC-{}".format(pgrec['webcnt'])
1442
+ if pgrec['dwebcnt']:
1443
+ if ws: msg += " DS-{}".format(pgrec['dweb_size'])
1444
+ if wn: msg += " DC-{}".format(pgrec['dwebcnt'])
1445
+ if pgrec['cdwcnt'] and wn: msg += " CDC-{}".format(pgrec['cdwcnt'])
1446
+ if pgrec['nwebcnt']:
1447
+ if ws: msg += " NS-{}".format(pgrec['nweb_size'])
1448
+ if wn: msg += " NC-{}".format(pgrec['nwebcnt'])
1449
+ if wn: msg += " WC-{}".format(pgrec['webcnt'])
1438
1450
  if pgrec['savedcnt']:
1439
- msg += " SS-{} SC-{}".format(pgrec['saved_size'], pgrec['savedcnt'])
1440
- if msg: print("{}:{}".format(head, msg))
1451
+ if ws: msg += " SS-{}".format(pgrec['saved_size'])
1452
+ if wn: msg += " SC-{}".format(pgrec['savedcnt'])
1453
+ if msg: self.OUTPUT.write("{}:{}\n".format(head, msg))
1454
+ # group counts include the sub groups, so check the files of all of them
1455
+ gidxs = self.get_all_subgroups(dcnd, grecs[i]) if grecs[i] else None
1456
+ self.check_file_storage(dsid, head, gidxs, wn, ws)
1457
+
1458
+ # Quasar backup status of a file, by bfile.type and bfile.status of its bid
1459
+ QBSTATS = (('B', 'Quasar Backup'), ('D', 'Quasar Backup&DR'),
1460
+ ('P', 'Quasar Backup Pending'), ('N', 'Not Backed Up'))
1461
+ # where a missing file was looked for, by locflag; 'C' (CGD) and 'R' (remote) files
1462
+ # are not stored by GDEX and are checked neither on storage nor on Quasar
1463
+ # 'M' is the total of files missing, each counted once even when missing in both
1464
+ # places; a saved file found in COLD_STORAGE is not missing
1465
+ # 'Q' are the missing files that can still be restored from a finished Quasar backup,
1466
+ # so 'M' minus 'Q' is what is lost
1467
+ QMSTATS = (('M', 'Missing'), ('Q', 'Missing but on Quasar'), ('G', 'Missing on Glade'),
1468
+ ('O', 'Missing on Boreas'), ('C', 'In COLD_STORAGE'),
1469
+ ('X', 'Not Checked (CGD/Remote)'))
1470
+
1471
+ def check_file_storage(self, dsid, head, gidxs, wn, ws):
1472
+ """
1473
+ Check the web and saved files of a dataset or group on storage and Quasar.
1474
+
1475
+ For every web/saved file that is not deleted: a Glade copy (locflag G or B)
1476
+ must exist under the dataset home, a Boreas copy (locflag O or B) must exist
1477
+ in the object store, and a Glade saved file is looked for under COLD_STORAGE,
1478
+ counted there whether or not it is still on Glade too. The Quasar backup status comes from bfile.type ('B' backup,
1479
+ 'D' backup and disaster recovery) of the file's bid, counted only once the
1480
+ backup file status is 'A'. Writes the totals and every missing or not
1481
+ backed up file to self.OUTPUT.
1482
+
1483
+ Args:
1484
+ dsid -- dataset ID string
1485
+ head -- line prefix, dsid or dsid-G<gindex>
1486
+ gidxs -- group indices to check, None for the whole dataset
1487
+ wn -- show file numbers (-WN)
1488
+ ws -- show file sizes (-WS)
1489
+ """
1490
+ if gidxs is None:
1491
+ gcnd = ''
1492
+ elif len(gidxs) == 1:
1493
+ gcnd = " AND {{0}}.gindex = {}".format(gidxs[0])
1494
+ else:
1495
+ gcnd = " AND {{0}}.gindex IN ({})".format(', '.join(str(g) for g in gidxs))
1496
+ bflds = "bfile.type btype, bfile.status bstat"
1497
+ bjoin = "LEFT JOIN bfile ON {0}.bid = bfile.bid"
1498
+ wrecs = self.pgmget_wfile_join(dsid, bjoin.format('wfile'),
1499
+ "wfile.wid fid, wfile.wfile fname, wfile.type, wfile.data_size, " +
1500
+ "wfile.locflag, wfile.bid, " + bflds,
1501
+ "wfile.status <> 'D'" + gcnd.format('wfile'), self.PGOPT['extlog'])
1502
+ srecs = self.pgmget("sfile " + bjoin.format('sfile'),
1503
+ "sfile.sid fid, sfile.sfile fname, sfile.type, sfile.data_size, " +
1504
+ "sfile.locflag, sfile.bid, " + bflds,
1505
+ "sfile.dsid = '{}' AND sfile.status <> 'D'".format(dsid) + gcnd.format('sfile'),
1506
+ self.PGOPT['extlog'])
1507
+ for (cate, frecs) in (('W', wrecs), ('S', srecs)):
1508
+ fcnt = len(frecs['fid']) if frecs else 0
1509
+ if fcnt: self.view_file_storage(dsid, head, cate, frecs, fcnt, wn, ws)
1510
+
1511
+ def view_file_storage(self, dsid, head, cate, frecs, fcnt, wn, ws):
1512
+ """
1513
+ Check one category (W-web or S-saved) of file records and write the report.
1514
+
1515
+ Args:
1516
+ dsid -- dataset ID string
1517
+ head -- line prefix, dsid or dsid-G<gindex>
1518
+ cate -- 'W' for web files, 'S' for saved files
1519
+ frecs -- multi-record dict from check_file_storage()
1520
+ fcnt -- number of records in frecs
1521
+ wn -- show file numbers (-WN)
1522
+ ws -- show file sizes (-WS)
1523
+ """
1524
+ self.cache_rtpath(dsid)
1525
+ bucket = 'gdex-decsdata' if cate == 'S' else self.PGLOG['OBJCTBKT']
1526
+ cname = 'Saved' if cate == 'S' else 'Web'
1527
+ total = [0, 0]
1528
+ bcnts = {k: [0, 0] for (k, m) in self.QBSTATS}
1529
+ mcnts = {k: [0, 0] for (k, m) in self.QMSTATS}
1530
+ lines = []
1531
+ for i in range(fcnt):
1532
+ fname = frecs['fname'][i]
1533
+ ftype = frecs['type'][i]
1534
+ fsize = frecs['data_size'][i] or 0
1535
+ locflag = frecs['locflag'][i]
1536
+ finfo = "{}-{}-{}: {}".format(head, cate, ftype, fname)
1537
+ total[0] += 1
1538
+ total[1] += fsize
1539
+ if locflag in ('C', 'R'):
1540
+ # not stored by GDEX, so neither on storage nor on Quasar
1541
+ mcnts['X'][0] += 1
1542
+ mcnts['X'][1] += fsize
1543
+ continue
1544
+ else:
1545
+ # quasar backup, by the backup file of the bid
1546
+ if not frecs['bid'][i]:
1547
+ bkey = 'N'
1548
+ elif frecs['bstat'][i] == 'A' and frecs['btype'][i] in ('B', 'D'):
1549
+ bkey = frecs['btype'][i]
1550
+ else:
1551
+ bkey = 'P'
1552
+ bcnts[bkey][0] += 1
1553
+ bcnts[bkey][1] += fsize
1554
+ bmsg = dict(self.QBSTATS)[bkey]
1555
+ if bkey != 'N': bmsg += " (Backup ID {})".format(frecs['bid'][i])
1556
+ # physical copies
1557
+ msgs = []
1558
+ missed = False
1559
+ if locflag != 'O':
1560
+ if cate == 'S':
1561
+ lfile = "{}/{}/{}".format(self.RTPATH['SP'], ftype, fname)
1562
+ else:
1563
+ lfile = self.join_paths(self.RTPATH.get(ftype, self.RTPATH['D']), fname)
1564
+ # every Glade saved file is looked for in cold storage, so the count tells
1565
+ # how much of the saved data is on tape, whether or not it is on Glade too
1566
+ cfile = self.cold_storage_file(dsid, ftype, fname) if cate == 'S' else None
1567
+ if cfile:
1568
+ mcnts['C'][0] += 1
1569
+ mcnts['C'][1] += fsize
1570
+ if not op.exists(lfile):
1571
+ mcnts['G'][0] += 1
1572
+ mcnts['G'][1] += fsize
1573
+ msg = self.QMSTATS[2][1]
1574
+ if cfile:
1575
+ msg += ", Found in COLD_STORAGE " + cfile
1576
+ else:
1577
+ missed = True
1578
+ msgs.append(msg)
1579
+ if locflag in ('O', 'B'):
1580
+ if not self.object_file_exists(dsid, fname, bucket):
1581
+ mcnts['O'][0] += 1
1582
+ mcnts['O'][1] += fsize
1583
+ msgs.append(self.QMSTATS[3][1])
1584
+ missed = True
1585
+ if missed:
1586
+ mcnts['M'][0] += 1
1587
+ mcnts['M'][1] += fsize
1588
+ if bkey in ('B', 'D'):
1589
+ mcnts['Q'][0] += 1
1590
+ mcnts['Q'][1] += fsize
1591
+ # one line per file: where it is missing and where it is backed up, so a
1592
+ # missing file shows whether it can be restored; a present file is listed
1593
+ # only when it is not backed up yet
1594
+ if msgs:
1595
+ msgs.append("on " + bmsg if bkey in ('B', 'D') else bmsg)
1596
+ elif bkey in ('N', 'P'):
1597
+ msgs.append(bmsg)
1598
+ if msgs: lines.append("{}: {}".format(finfo, '; '.join(msgs)))
1599
+ msg = "{} {} Files: Total {}".format(head, cname, self.file_number_size(total, wn, ws))
1600
+ for (k, m) in self.QMSTATS:
1601
+ # the total missing is always shown, so a clean check reads 'Missing 0'
1602
+ if mcnts[k][0] or k == 'M': msg += ", {} {}".format(m, self.file_number_size(mcnts[k], wn, ws))
1603
+ for (k, m) in self.QBSTATS:
1604
+ if bcnts[k][0]: msg += ", {} {}".format(m, self.file_number_size(bcnts[k], wn, ws))
1605
+ self.OUTPUT.write(msg + "\n")
1606
+ for line in lines:
1607
+ self.OUTPUT.write(line + "\n")
1608
+
1609
+ @staticmethod
1610
+ def file_number_size(cnts, wn, ws):
1611
+ """Format [count, size] as 'count', 'size' or 'count(size)' per -WN/-WS."""
1612
+ if wn and ws: return "{}({})".format(cnts[0], cnts[1])
1613
+ return str(cnts[1] if ws else cnts[0])
1614
+
1615
+ def object_file_exists(self, dsid, fname, bucket):
1616
+ """
1617
+ Check whether an object key, or a directory key such as a zarr store, exists.
1618
+
1619
+ Lists the whole dataset prefix once per dataset and bucket, so checking a
1620
+ dataset of many object files costs one object store call instead of one per
1621
+ file. A listing error is reported once and the files are then taken as present.
1622
+ """
1623
+ ckey = "{}:{}".format(bucket, dsid)
1624
+ if ckey not in self.OBJKEYS:
1625
+ keys = set()
1626
+ flist = self.object_glob(dsid + '/', bucket, 0, self.PGOPT['wrnlog'])
1627
+ if flist == self.FAILURE:
1628
+ keys = None
1629
+ elif flist:
1630
+ for key in flist:
1631
+ # a key under a directory key also stands for each of its parent directories
1632
+ while key and key not in keys:
1633
+ keys.add(key)
1634
+ key = op.dirname(key)
1635
+ self.OBJKEYS[ckey] = keys
1636
+ keys = self.OBJKEYS[ckey]
1637
+ if keys is None: return True
1638
+ return self.join_paths(dsid, fname) in keys
1639
+
1640
+ def cold_storage_file(self, dsid, stype, sfile):
1641
+ """
1642
+ Find a saved file moved into GLADE cold storage.
1643
+
1644
+ A dataset is moved either to <DECSHOME>/COLD_STORAGE/<dsid> or to
1645
+ <DECSHOME>/cold_storage_<YYYYMMDD>/COLD_STORAGE/<dsid>; both are searched,
1646
+ the root one first and then the dated ones newest first. Checking a migrated
1647
+ file only reads its inode, it does not recall it from tape.
1648
+
1649
+ Returns:
1650
+ Path of the file in cold storage, or None if not found.
1651
+ """
1652
+ if dsid not in self.COLDDIRS:
1653
+ dhome = self.PGLOG['DECSHOME']
1654
+ cdirs = []
1655
+ for cname in ('COLD_STORAGE', 'COLD_STORAGE_SINGLE_COPY'):
1656
+ cdirs.append("{}/{}/{}".format(dhome, cname, dsid))
1657
+ for cdir in sorted(glob.glob(dhome + "/cold_storage_*"), reverse = True):
1658
+ for cname in ('COLD_STORAGE', 'COLD_STORAGE_SINGLE_COPY'):
1659
+ cdirs.append("{}/{}/{}".format(cdir, cname, dsid))
1660
+ self.COLDDIRS[dsid] = [cdir for cdir in cdirs if op.isdir(cdir)]
1661
+ for cdir in self.COLDDIRS[dsid]:
1662
+ cfile = "{}/{}/{}".format(cdir, stype, sfile)
1663
+ if op.exists(cfile): return cfile
1664
+ return None
1441
1665
 
1442
1666
  def get_bid_numbers(self, bfiles):
1443
1667
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsarch
3
- Version: 3.0.11
3
+ Version: 3.0.13
4
4
  Summary: RDA Python package for utility program dsarch archive RDA data
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-dsarch