cloud-files 6.2.2__tar.gz → 6.3.0__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.
- {cloud_files-6.2.2 → cloud_files-6.3.0}/ChangeLog +6 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/PKG-INFO +43 -2
- {cloud_files-6.2.2 → cloud_files-6.3.0}/README.md +29 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloud_files.egg-info/PKG-INFO +43 -2
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloud_files.egg-info/SOURCES.txt +2 -1
- cloud_files-6.3.0/cloud_files.egg-info/pbr.json +1 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/cloudfiles.py +13 -2
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/interfaces.py +158 -33
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles_cli/cloudfiles_cli.py +50 -29
- cloud_files-6.3.0/cloudfiles_cli/listing_db.py +174 -0
- cloud_files-6.2.2/cloud_files.egg-info/pbr.json +0 -1
- {cloud_files-6.2.2 → cloud_files-6.3.0}/.github/workflows/test-suite.yml +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/AUTHORS +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/LICENSE +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/MANIFEST.in +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/automated_test.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloud_files.egg-info/dependency_links.txt +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloud_files.egg-info/entry_points.txt +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloud_files.egg-info/not-zip-safe +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloud_files.egg-info/requires.txt +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloud_files.egg-info/top_level.txt +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/__init__.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/compression.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/connectionpools.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/exceptions.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/gcs.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/lib.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/monitoring.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/paths.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/resumable_tools.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/scheduler.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/secrets.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/threaded_queue.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles/typing.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles_cli/LICENSE +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/cloudfiles_cli/__init__.py +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/requirements.txt +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/setup.cfg +0 -0
- {cloud_files-6.2.2 → cloud_files-6.3.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: cloud-files
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.3.0
|
|
4
4
|
Summary: Fast access to cloud storage and local FS.
|
|
5
5
|
Home-page: https://github.com/seung-lab/cloud-files/
|
|
6
6
|
Author: William Silversmith
|
|
@@ -54,6 +54,18 @@ Requires-Dist: intervaltree; extra == "monitoring"
|
|
|
54
54
|
Requires-Dist: matplotlib; extra == "monitoring"
|
|
55
55
|
Provides-Extra: apache
|
|
56
56
|
Requires-Dist: lxml; extra == "apache"
|
|
57
|
+
Dynamic: author
|
|
58
|
+
Dynamic: author-email
|
|
59
|
+
Dynamic: classifier
|
|
60
|
+
Dynamic: description
|
|
61
|
+
Dynamic: description-content-type
|
|
62
|
+
Dynamic: home-page
|
|
63
|
+
Dynamic: license
|
|
64
|
+
Dynamic: license-file
|
|
65
|
+
Dynamic: provides-extra
|
|
66
|
+
Dynamic: requires-dist
|
|
67
|
+
Dynamic: requires-python
|
|
68
|
+
Dynamic: summary
|
|
57
69
|
|
|
58
70
|
[](https://badge.fury.io/py/cloud-files) [](https://github.com/seung-lab/cloud-files/actions?query=workflow%3A%22Test+Suite%22)
|
|
59
71
|
|
|
@@ -497,6 +509,8 @@ cf.clear_locks()
|
|
|
497
509
|
```bash
|
|
498
510
|
# list cloud and local directories
|
|
499
511
|
cloudfiles ls gs://bucket-folder/
|
|
512
|
+
# resumable list cloud and local directories to sqlite db
|
|
513
|
+
cloudfiles ls --sqlite example.db gs://bucket-folder/
|
|
500
514
|
# parallel file transfer, no decompression
|
|
501
515
|
cloudfiles -p 2 cp --progress -r s3://bkt/ gs://bkt2/
|
|
502
516
|
# change compression type to brotli
|
|
@@ -554,6 +568,33 @@ cloudfiles ls -e "gs://bucket/prefix[ab]"
|
|
|
554
568
|
# cloudfiles ls gs://bucket/prefixb
|
|
555
569
|
```
|
|
556
570
|
|
|
571
|
+
### `ls` sqlite
|
|
572
|
+
|
|
573
|
+
When there is a very large directory in the cloud, sometimes we want to download the file listing and file sizes locally for ease of searching and comparison. This feature allows you to initiate resumable downloads of a given bucket or directory on Google Cloud Storage or Amazone S3 endpoints. The `file`, `mem`, and `https` protocols do not support resumption (except for the Google Storage REST API which does have support).
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
cloudfiles ls --sqlite example.db gs://bucket/prefix --progress
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
This will start downloading the data to `example.db` in the local directory. You can then search for files in the `files` table.
|
|
580
|
+
|
|
581
|
+
```sql
|
|
582
|
+
SELECT sum(size) FROM files WHERE path LIKE '%example.jpg';
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
For example, one plausible use for this technique is to check whether a copy of a large dataset has missing files. Download `original.db` and `copy.db` and then you can do a set difference.
|
|
586
|
+
|
|
587
|
+
```sql
|
|
588
|
+
$ sqlite3
|
|
589
|
+
|
|
590
|
+
> ATTACH DATABASE 'original.db' AS original_data;
|
|
591
|
+
> ATTACH DATABASE 'copy.db' AS copied_data;
|
|
592
|
+
|
|
593
|
+
> SELECT path from original_data.files
|
|
594
|
+
EXCEPT
|
|
595
|
+
SELECT path from copied_data.files;
|
|
596
|
+
```
|
|
597
|
+
|
|
557
598
|
### `alias` for Alternative S3 Endpoints
|
|
558
599
|
|
|
559
600
|
You can set your own protocols for S3 compatible endpoints by creating dynamic or persistent aliases. CloudFiles comes with two official s3 endpoints that are important for the Seung Lab, `matrix://` and `tigerdata://` which point to Princeton S3 endpoints. Official aliases can't be overridden.
|
|
@@ -440,6 +440,8 @@ cf.clear_locks()
|
|
|
440
440
|
```bash
|
|
441
441
|
# list cloud and local directories
|
|
442
442
|
cloudfiles ls gs://bucket-folder/
|
|
443
|
+
# resumable list cloud and local directories to sqlite db
|
|
444
|
+
cloudfiles ls --sqlite example.db gs://bucket-folder/
|
|
443
445
|
# parallel file transfer, no decompression
|
|
444
446
|
cloudfiles -p 2 cp --progress -r s3://bkt/ gs://bkt2/
|
|
445
447
|
# change compression type to brotli
|
|
@@ -497,6 +499,33 @@ cloudfiles ls -e "gs://bucket/prefix[ab]"
|
|
|
497
499
|
# cloudfiles ls gs://bucket/prefixb
|
|
498
500
|
```
|
|
499
501
|
|
|
502
|
+
### `ls` sqlite
|
|
503
|
+
|
|
504
|
+
When there is a very large directory in the cloud, sometimes we want to download the file listing and file sizes locally for ease of searching and comparison. This feature allows you to initiate resumable downloads of a given bucket or directory on Google Cloud Storage or Amazone S3 endpoints. The `file`, `mem`, and `https` protocols do not support resumption (except for the Google Storage REST API which does have support).
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
cloudfiles ls --sqlite example.db gs://bucket/prefix --progress
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
This will start downloading the data to `example.db` in the local directory. You can then search for files in the `files` table.
|
|
511
|
+
|
|
512
|
+
```sql
|
|
513
|
+
SELECT sum(size) FROM files WHERE path LIKE '%example.jpg';
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
For example, one plausible use for this technique is to check whether a copy of a large dataset has missing files. Download `original.db` and `copy.db` and then you can do a set difference.
|
|
517
|
+
|
|
518
|
+
```sql
|
|
519
|
+
$ sqlite3
|
|
520
|
+
|
|
521
|
+
> ATTACH DATABASE 'original.db' AS original_data;
|
|
522
|
+
> ATTACH DATABASE 'copy.db' AS copied_data;
|
|
523
|
+
|
|
524
|
+
> SELECT path from original_data.files
|
|
525
|
+
EXCEPT
|
|
526
|
+
SELECT path from copied_data.files;
|
|
527
|
+
```
|
|
528
|
+
|
|
500
529
|
### `alias` for Alternative S3 Endpoints
|
|
501
530
|
|
|
502
531
|
You can set your own protocols for S3 compatible endpoints by creating dynamic or persistent aliases. CloudFiles comes with two official s3 endpoints that are important for the Seung Lab, `matrix://` and `tigerdata://` which point to Princeton S3 endpoints. Official aliases can't be overridden.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: cloud-files
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.3.0
|
|
4
4
|
Summary: Fast access to cloud storage and local FS.
|
|
5
5
|
Home-page: https://github.com/seung-lab/cloud-files/
|
|
6
6
|
Author: William Silversmith
|
|
@@ -54,6 +54,18 @@ Requires-Dist: intervaltree; extra == "monitoring"
|
|
|
54
54
|
Requires-Dist: matplotlib; extra == "monitoring"
|
|
55
55
|
Provides-Extra: apache
|
|
56
56
|
Requires-Dist: lxml; extra == "apache"
|
|
57
|
+
Dynamic: author
|
|
58
|
+
Dynamic: author-email
|
|
59
|
+
Dynamic: classifier
|
|
60
|
+
Dynamic: description
|
|
61
|
+
Dynamic: description-content-type
|
|
62
|
+
Dynamic: home-page
|
|
63
|
+
Dynamic: license
|
|
64
|
+
Dynamic: license-file
|
|
65
|
+
Dynamic: provides-extra
|
|
66
|
+
Dynamic: requires-dist
|
|
67
|
+
Dynamic: requires-python
|
|
68
|
+
Dynamic: summary
|
|
57
69
|
|
|
58
70
|
[](https://badge.fury.io/py/cloud-files) [](https://github.com/seung-lab/cloud-files/actions?query=workflow%3A%22Test+Suite%22)
|
|
59
71
|
|
|
@@ -497,6 +509,8 @@ cf.clear_locks()
|
|
|
497
509
|
```bash
|
|
498
510
|
# list cloud and local directories
|
|
499
511
|
cloudfiles ls gs://bucket-folder/
|
|
512
|
+
# resumable list cloud and local directories to sqlite db
|
|
513
|
+
cloudfiles ls --sqlite example.db gs://bucket-folder/
|
|
500
514
|
# parallel file transfer, no decompression
|
|
501
515
|
cloudfiles -p 2 cp --progress -r s3://bkt/ gs://bkt2/
|
|
502
516
|
# change compression type to brotli
|
|
@@ -554,6 +568,33 @@ cloudfiles ls -e "gs://bucket/prefix[ab]"
|
|
|
554
568
|
# cloudfiles ls gs://bucket/prefixb
|
|
555
569
|
```
|
|
556
570
|
|
|
571
|
+
### `ls` sqlite
|
|
572
|
+
|
|
573
|
+
When there is a very large directory in the cloud, sometimes we want to download the file listing and file sizes locally for ease of searching and comparison. This feature allows you to initiate resumable downloads of a given bucket or directory on Google Cloud Storage or Amazone S3 endpoints. The `file`, `mem`, and `https` protocols do not support resumption (except for the Google Storage REST API which does have support).
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
cloudfiles ls --sqlite example.db gs://bucket/prefix --progress
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
This will start downloading the data to `example.db` in the local directory. You can then search for files in the `files` table.
|
|
580
|
+
|
|
581
|
+
```sql
|
|
582
|
+
SELECT sum(size) FROM files WHERE path LIKE '%example.jpg';
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
For example, one plausible use for this technique is to check whether a copy of a large dataset has missing files. Download `original.db` and `copy.db` and then you can do a set difference.
|
|
586
|
+
|
|
587
|
+
```sql
|
|
588
|
+
$ sqlite3
|
|
589
|
+
|
|
590
|
+
> ATTACH DATABASE 'original.db' AS original_data;
|
|
591
|
+
> ATTACH DATABASE 'copy.db' AS copied_data;
|
|
592
|
+
|
|
593
|
+
> SELECT path from original_data.files
|
|
594
|
+
EXCEPT
|
|
595
|
+
SELECT path from copied_data.files;
|
|
596
|
+
```
|
|
597
|
+
|
|
557
598
|
### `alias` for Alternative S3 Endpoints
|
|
558
599
|
|
|
559
600
|
You can set your own protocols for S3 compatible endpoints by creating dynamic or persistent aliases. CloudFiles comes with two official s3 endpoints that are important for the Seung Lab, `matrix://` and `tigerdata://` which point to Princeton S3 endpoints. Official aliases can't be overridden.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"git_version": "182afad", "is_release": true}
|
|
@@ -1138,7 +1138,12 @@ class CloudFiles:
|
|
|
1138
1138
|
# )
|
|
1139
1139
|
|
|
1140
1140
|
def list(
|
|
1141
|
-
self,
|
|
1141
|
+
self,
|
|
1142
|
+
prefix:str = "",
|
|
1143
|
+
flat:bool = False,
|
|
1144
|
+
size:bool = False,
|
|
1145
|
+
return_resume_token:bool = False,
|
|
1146
|
+
resume_token:Optional[str] = None,
|
|
1142
1147
|
) -> Generator[str,None,None]:
|
|
1143
1148
|
"""
|
|
1144
1149
|
List files with the given prefix.
|
|
@@ -1160,7 +1165,13 @@ class CloudFiles:
|
|
|
1160
1165
|
Return: generated sequence of file paths relative to cloudpath
|
|
1161
1166
|
"""
|
|
1162
1167
|
with self._get_connection() as conn:
|
|
1163
|
-
for f in conn.list_files(
|
|
1168
|
+
for f in conn.list_files(
|
|
1169
|
+
prefix=prefix,
|
|
1170
|
+
flat=flat,
|
|
1171
|
+
size=size,
|
|
1172
|
+
resume_token=resume_token,
|
|
1173
|
+
return_resume_token=return_resume_token,
|
|
1174
|
+
):
|
|
1164
1175
|
yield f
|
|
1165
1176
|
|
|
1166
1177
|
def transfer_to(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
1
3
|
import base64
|
|
2
4
|
import binascii
|
|
3
5
|
from collections import defaultdict, namedtuple
|
|
@@ -346,7 +348,14 @@ class FileInterface(StorageInterface):
|
|
|
346
348
|
for path in file_paths:
|
|
347
349
|
self.delete_file(path)
|
|
348
350
|
|
|
349
|
-
def list_files(
|
|
351
|
+
def list_files(
|
|
352
|
+
self,
|
|
353
|
+
prefix:str,
|
|
354
|
+
flat:bool = False,
|
|
355
|
+
size:bool = False,
|
|
356
|
+
return_resume_token:bool = False,
|
|
357
|
+
resume_token:Optional[str] = None,
|
|
358
|
+
):
|
|
350
359
|
"""
|
|
351
360
|
List the files in the layer with the given prefix.
|
|
352
361
|
|
|
@@ -354,7 +363,6 @@ class FileInterface(StorageInterface):
|
|
|
354
363
|
while non-flat means generate all file paths with that
|
|
355
364
|
prefix.
|
|
356
365
|
"""
|
|
357
|
-
|
|
358
366
|
layer_path = self.get_path_to_file("")
|
|
359
367
|
path = os.path.join(layer_path, prefix)
|
|
360
368
|
|
|
@@ -405,8 +413,16 @@ class FileInterface(StorageInterface):
|
|
|
405
413
|
|
|
406
414
|
filenames = list(map(stripext, filenames))
|
|
407
415
|
filenames.sort()
|
|
408
|
-
|
|
409
|
-
|
|
416
|
+
|
|
417
|
+
if not size and not return_resume_token:
|
|
418
|
+
return iter(filenames)
|
|
419
|
+
elif size and return_resume_token:
|
|
420
|
+
return ( (filename, os.path.getsize(filename), None) for filename in filenames )
|
|
421
|
+
elif size and not return_resume_token:
|
|
422
|
+
return ( (filename, os.path.getsize(filename)) for filename in filenames )
|
|
423
|
+
elif not size and return_resume_token:
|
|
424
|
+
return ( (filename, None) for filename in filenames )
|
|
425
|
+
|
|
410
426
|
class MemoryInterface(StorageInterface):
|
|
411
427
|
def __init__(self, path, secrets=None, request_payer=None, **kwargs):
|
|
412
428
|
global MEM_BUCKET_POOL_LOCK
|
|
@@ -589,7 +605,14 @@ class MemoryInterface(StorageInterface):
|
|
|
589
605
|
for path in file_paths:
|
|
590
606
|
self.delete_file(path)
|
|
591
607
|
|
|
592
|
-
def list_files(
|
|
608
|
+
def list_files(
|
|
609
|
+
self,
|
|
610
|
+
prefix:str,
|
|
611
|
+
flat:bool = False,
|
|
612
|
+
size:bool = False,
|
|
613
|
+
return_resume_token:bool = False,
|
|
614
|
+
resume_token:Optional[str] = None,
|
|
615
|
+
):
|
|
593
616
|
"""
|
|
594
617
|
List the files in the layer with the given prefix.
|
|
595
618
|
|
|
@@ -631,7 +654,17 @@ class MemoryInterface(StorageInterface):
|
|
|
631
654
|
|
|
632
655
|
filenames = list(map(stripext, filenames))
|
|
633
656
|
filenames.sort()
|
|
634
|
-
|
|
657
|
+
|
|
658
|
+
# The size operation could be made much faster
|
|
659
|
+
# but will require some surgery
|
|
660
|
+
if not size and not return_resume_token:
|
|
661
|
+
return iter(filenames)
|
|
662
|
+
elif size and return_resume_token:
|
|
663
|
+
return ( (filename, self.size(filename), None) for filename in filenames )
|
|
664
|
+
elif size and not return_resume_token:
|
|
665
|
+
return ( (filename, self.size(filename)) for filename in filenames )
|
|
666
|
+
elif not size and return_resume_token:
|
|
667
|
+
return ( (filename, None) for filename in filenames )
|
|
635
668
|
|
|
636
669
|
def subtree_size(self, prefix:str = "") -> tuple[int,int]:
|
|
637
670
|
layer_path = self.get_path_to_file("")
|
|
@@ -833,9 +866,15 @@ class GoogleCloudStorageInterface(StorageInterface):
|
|
|
833
866
|
except google.cloud.exceptions.NotFound:
|
|
834
867
|
pass
|
|
835
868
|
|
|
836
|
-
|
|
837
869
|
@retry
|
|
838
|
-
def list_files(
|
|
870
|
+
def list_files(
|
|
871
|
+
self,
|
|
872
|
+
prefix:str,
|
|
873
|
+
flat:bool = False,
|
|
874
|
+
size:bool = False,
|
|
875
|
+
return_resume_token:bool = False,
|
|
876
|
+
resume_token:Optional[str] = None,
|
|
877
|
+
):
|
|
839
878
|
"""
|
|
840
879
|
List the files in the layer with the given prefix.
|
|
841
880
|
|
|
@@ -848,29 +887,53 @@ class GoogleCloudStorageInterface(StorageInterface):
|
|
|
848
887
|
|
|
849
888
|
delimiter = '/' if flat else None
|
|
850
889
|
|
|
890
|
+
items = "name"
|
|
891
|
+
if size:
|
|
892
|
+
items += ",size"
|
|
893
|
+
|
|
851
894
|
blobs = self._bucket.list_blobs(
|
|
852
895
|
prefix=path,
|
|
853
896
|
delimiter=delimiter,
|
|
854
897
|
page_size=2500,
|
|
855
|
-
fields="items(
|
|
898
|
+
fields=f"items({items}),nextPageToken,prefixes",
|
|
899
|
+
page_token=resume_token,
|
|
856
900
|
)
|
|
857
901
|
|
|
902
|
+
def return_args(filename, blob, page):
|
|
903
|
+
nonlocal blobs
|
|
904
|
+
args = [ filename ]
|
|
905
|
+
if size:
|
|
906
|
+
args.append(blob.size)
|
|
907
|
+
if return_resume_token:
|
|
908
|
+
args.append(blobs.next_page_token)
|
|
909
|
+
|
|
910
|
+
if len(args) == 1:
|
|
911
|
+
return args[0]
|
|
912
|
+
else:
|
|
913
|
+
return tuple(args)
|
|
914
|
+
|
|
858
915
|
for page in blobs.pages:
|
|
859
916
|
if page.prefixes:
|
|
860
|
-
|
|
861
|
-
item.removeprefix(path)
|
|
862
|
-
|
|
863
|
-
|
|
917
|
+
for item in page.prefixes:
|
|
918
|
+
ret = [ item.removeprefix(path) ]
|
|
919
|
+
if size:
|
|
920
|
+
ret.append(0)
|
|
921
|
+
if return_resume_token:
|
|
922
|
+
ret.append(page.next_page_token)
|
|
923
|
+
|
|
924
|
+
if len(ret) == 1:
|
|
925
|
+
yield ret[0]
|
|
926
|
+
else:
|
|
927
|
+
yield tuple(ret)
|
|
864
928
|
|
|
865
929
|
for blob in page:
|
|
866
930
|
filename = blob.name.removeprefix(layer_path)
|
|
867
931
|
if not filename:
|
|
868
932
|
continue
|
|
869
933
|
elif not flat and filename[-1] != '/':
|
|
870
|
-
yield filename
|
|
934
|
+
yield return_args(filename, blob, page)
|
|
871
935
|
elif flat and '/' not in blob.name.removeprefix(path):
|
|
872
|
-
yield filename
|
|
873
|
-
|
|
936
|
+
yield return_args(filename, blob, page)
|
|
874
937
|
|
|
875
938
|
@retry
|
|
876
939
|
def subtree_size(self, prefix:str = "") -> tuple[int,int]:
|
|
@@ -1026,7 +1089,14 @@ class HttpInterface(StorageInterface):
|
|
|
1026
1089
|
def files_exist(self, file_paths):
|
|
1027
1090
|
return {path: self.exists(path) for path in file_paths}
|
|
1028
1091
|
|
|
1029
|
-
def _list_files_google(
|
|
1092
|
+
def _list_files_google(
|
|
1093
|
+
self,
|
|
1094
|
+
prefix:str,
|
|
1095
|
+
flat:bool = False,
|
|
1096
|
+
size:bool = False,
|
|
1097
|
+
return_resume_token:bool = False,
|
|
1098
|
+
resume_token:Optional[str] = None,
|
|
1099
|
+
):
|
|
1030
1100
|
bucket = self._path.path.split('/')[0]
|
|
1031
1101
|
prefix = posixpath.join(
|
|
1032
1102
|
self._path.path.replace(bucket, '', 1),
|
|
@@ -1037,10 +1107,18 @@ class HttpInterface(StorageInterface):
|
|
|
1037
1107
|
|
|
1038
1108
|
headers = self.default_headers()
|
|
1039
1109
|
|
|
1110
|
+
items = "name"
|
|
1111
|
+
if size:
|
|
1112
|
+
items += ",size"
|
|
1113
|
+
|
|
1114
|
+
fields = f"items({items}),nextPageToken,prefixes"
|
|
1115
|
+
|
|
1040
1116
|
@retry_if_not(AuthorizationError)
|
|
1041
1117
|
def request(token):
|
|
1042
1118
|
nonlocal headers
|
|
1043
|
-
params = {
|
|
1119
|
+
params = {
|
|
1120
|
+
"fields": fields,
|
|
1121
|
+
}
|
|
1044
1122
|
if prefix:
|
|
1045
1123
|
params["prefix"] = prefix
|
|
1046
1124
|
if token is not None:
|
|
@@ -1048,6 +1126,7 @@ class HttpInterface(StorageInterface):
|
|
|
1048
1126
|
if flat:
|
|
1049
1127
|
params["delimiter"] = '/'
|
|
1050
1128
|
|
|
1129
|
+
|
|
1051
1130
|
results = self.session.get(
|
|
1052
1131
|
f"https://storage.googleapis.com/storage/v1/b/{bucket}/o",
|
|
1053
1132
|
params=params,
|
|
@@ -1067,17 +1146,33 @@ class HttpInterface(StorageInterface):
|
|
|
1067
1146
|
token = None
|
|
1068
1147
|
while True:
|
|
1069
1148
|
results = request(token)
|
|
1149
|
+
token = results.get("nextPageToken", None)
|
|
1070
1150
|
|
|
1071
1151
|
if 'prefixes' in results:
|
|
1072
|
-
|
|
1152
|
+
itr = (
|
|
1073
1153
|
item.removeprefix(strip)
|
|
1074
1154
|
for item in results["prefixes"]
|
|
1075
1155
|
)
|
|
1156
|
+
if not size and not return_resume_token:
|
|
1157
|
+
yield from itr
|
|
1158
|
+
elif not size and return_resume_token:
|
|
1159
|
+
yield from ( (pre, token) for pre in itr )
|
|
1160
|
+
elif size and not return_resume_token:
|
|
1161
|
+
yield from ( (pre, 0) for pre in itr )
|
|
1162
|
+
else:
|
|
1163
|
+
yield from ( (pre, 0, token) for pre in itr )
|
|
1076
1164
|
|
|
1077
1165
|
for res in results.get("items", []):
|
|
1078
|
-
|
|
1166
|
+
name = res["name"].removeprefix(strip)
|
|
1167
|
+
if not size and not return_resume_token:
|
|
1168
|
+
yield name
|
|
1169
|
+
elif size and not return_resume_token:
|
|
1170
|
+
yield (name, int(res["size"]))
|
|
1171
|
+
elif not size and return_resume_token:
|
|
1172
|
+
yield (name, token)
|
|
1173
|
+
else:
|
|
1174
|
+
yield (name, int(res["size"]), token)
|
|
1079
1175
|
|
|
1080
|
-
token = results.get("nextPageToken", None)
|
|
1081
1176
|
if token is None:
|
|
1082
1177
|
break
|
|
1083
1178
|
|
|
@@ -1124,11 +1219,21 @@ class HttpInterface(StorageInterface):
|
|
|
1124
1219
|
if flat:
|
|
1125
1220
|
break
|
|
1126
1221
|
|
|
1127
|
-
def list_files(
|
|
1222
|
+
def list_files(
|
|
1223
|
+
self,
|
|
1224
|
+
prefix:str,
|
|
1225
|
+
flat:bool = False,
|
|
1226
|
+
size:bool = False,
|
|
1227
|
+
return_resume_token:bool = False,
|
|
1228
|
+
resume_token:Optional[str] = None,
|
|
1229
|
+
):
|
|
1128
1230
|
if self._path.host == "https://storage.googleapis.com":
|
|
1129
|
-
yield from self._list_files_google(prefix, flat)
|
|
1231
|
+
yield from self._list_files_google(prefix, flat, size, return_resume_token, resume_token)
|
|
1130
1232
|
return
|
|
1131
1233
|
|
|
1234
|
+
if size or resume_token or return_resume_token:
|
|
1235
|
+
raise NotImplementedError("size, resume_token, and return_resume_token are not yet implemented.")
|
|
1236
|
+
|
|
1132
1237
|
url = posixpath.join(self._path.host, self._path.path, prefix)
|
|
1133
1238
|
resp = requests.head(url)
|
|
1134
1239
|
|
|
@@ -1490,7 +1595,14 @@ class S3Interface(StorageInterface):
|
|
|
1490
1595
|
**self._additional_attrs,
|
|
1491
1596
|
)
|
|
1492
1597
|
|
|
1493
|
-
def list_files(
|
|
1598
|
+
def list_files(
|
|
1599
|
+
self,
|
|
1600
|
+
prefix:str,
|
|
1601
|
+
flat:bool = False,
|
|
1602
|
+
size:bool = False,
|
|
1603
|
+
return_resume_token:bool = False,
|
|
1604
|
+
resume_token:Optional[str] = None,
|
|
1605
|
+
):
|
|
1494
1606
|
"""
|
|
1495
1607
|
List the files in the layer with the given prefix.
|
|
1496
1608
|
|
|
@@ -1517,7 +1629,7 @@ class S3Interface(StorageInterface):
|
|
|
1517
1629
|
|
|
1518
1630
|
return self._conn.list_objects_v2(**kwargs)
|
|
1519
1631
|
|
|
1520
|
-
resp = s3lst(path)
|
|
1632
|
+
resp = s3lst(path, continuation_token=resume_token)
|
|
1521
1633
|
# the case where the prefix is something like "build", but "build" is a subdirectory
|
|
1522
1634
|
# so requery with "build/" to get the proper behavior
|
|
1523
1635
|
if (
|
|
@@ -1528,7 +1640,7 @@ class S3Interface(StorageInterface):
|
|
|
1528
1640
|
and len(resp.get("CommonPrefixes", [])) == 1
|
|
1529
1641
|
):
|
|
1530
1642
|
path += '/'
|
|
1531
|
-
resp = s3lst(path)
|
|
1643
|
+
resp = s3lst(path, continuation_token=resume_token)
|
|
1532
1644
|
|
|
1533
1645
|
def iterate(resp):
|
|
1534
1646
|
if 'CommonPrefixes' in resp.keys():
|
|
@@ -1540,24 +1652,37 @@ class S3Interface(StorageInterface):
|
|
|
1540
1652
|
if 'Contents' not in resp.keys():
|
|
1541
1653
|
resp['Contents'] = []
|
|
1542
1654
|
|
|
1655
|
+
token = None
|
|
1656
|
+
if resp["IsTruncated"]:
|
|
1657
|
+
token = resp["NextContinuationToken"]
|
|
1658
|
+
|
|
1543
1659
|
for item in resp['Contents']:
|
|
1544
1660
|
key = item['Key']
|
|
1545
1661
|
filename = key.removeprefix(layer_path)
|
|
1546
1662
|
if filename == '':
|
|
1547
1663
|
continue
|
|
1548
|
-
elif
|
|
1549
|
-
|
|
1550
|
-
elif flat and
|
|
1664
|
+
elif flat and '/' in key.removeprefix(path):
|
|
1665
|
+
continue
|
|
1666
|
+
elif not flat and filename[-1] == "/":
|
|
1667
|
+
continue
|
|
1668
|
+
|
|
1669
|
+
if not size and not return_resume_token:
|
|
1551
1670
|
yield filename
|
|
1671
|
+
elif size and not return_resume_token:
|
|
1672
|
+
yield (filename, int(item["Size"]))
|
|
1673
|
+
elif not size and return_resume_token:
|
|
1674
|
+
yield (filename, token)
|
|
1675
|
+
else:
|
|
1676
|
+
yield (filename, int(item["Size"]), token)
|
|
1552
1677
|
|
|
1553
|
-
for
|
|
1554
|
-
yield
|
|
1678
|
+
for result in iterate(resp):
|
|
1679
|
+
yield result
|
|
1555
1680
|
|
|
1556
1681
|
while resp['IsTruncated'] and resp['NextContinuationToken']:
|
|
1557
1682
|
resp = s3lst(path, resp['NextContinuationToken'])
|
|
1558
1683
|
|
|
1559
|
-
for
|
|
1560
|
-
yield
|
|
1684
|
+
for result in iterate(resp):
|
|
1685
|
+
yield result
|
|
1561
1686
|
|
|
1562
1687
|
def subtree_size(self, prefix:str = "") -> tuple[int,int]:
|
|
1563
1688
|
layer_path = self.get_path_to_file("")
|
|
@@ -37,6 +37,24 @@ from cloudfiles.lib import (
|
|
|
37
37
|
)
|
|
38
38
|
import cloudfiles.lib
|
|
39
39
|
|
|
40
|
+
from . import listing_db
|
|
41
|
+
|
|
42
|
+
def SI(val:int) -> str:
|
|
43
|
+
if val < 1024:
|
|
44
|
+
return f"{val} bytes"
|
|
45
|
+
elif val < 2**20:
|
|
46
|
+
return f"{(val / 2**10):.2f} KiB"
|
|
47
|
+
elif val < 2**30:
|
|
48
|
+
return f"{(val / 2**20):.2f} MiB"
|
|
49
|
+
elif val < 2**40:
|
|
50
|
+
return f"{(val / 2**30):.2f} GiB"
|
|
51
|
+
elif val < 2**50:
|
|
52
|
+
return f"{(val / 2**40):.2f} TiB"
|
|
53
|
+
elif val < 2**60:
|
|
54
|
+
return f"{(val / 2**50):.2f} PiB"
|
|
55
|
+
else:
|
|
56
|
+
return f"{(val / 2**60):.2f} EiB"
|
|
57
|
+
|
|
40
58
|
def cloudpathjoin(cloudpath, *args):
|
|
41
59
|
cloudpath = normalize_path(cloudpath)
|
|
42
60
|
proto = get_protocol(cloudpath)
|
|
@@ -89,8 +107,10 @@ def license():
|
|
|
89
107
|
@click.option('--flat', is_flag=True, default=False, help='Only produce a single level of directory hierarchy.',show_default=True)
|
|
90
108
|
@click.option('-e','--expr',is_flag=True, default=False, help=r'Use a limited regexp language (e.g. [abc123]{3}) to generate prefixes.', show_default=True)
|
|
91
109
|
@click.option('--no-auth',is_flag=True, default=False, help='Uses the http API for read-only operations.', show_default=True)
|
|
110
|
+
@click.option('--sqlite', 'sqlite_filename', default="", help='Write results to this sqlite database.', show_default=True)
|
|
111
|
+
@click.option('--progress', is_flag=True, default=False, help="Show progress bar for sqlite.", show_default=True)
|
|
92
112
|
@click.argument("cloudpath")
|
|
93
|
-
def ls(shortpath, flat, expr, cloudpath, no_auth):
|
|
113
|
+
def ls(shortpath, flat, expr, cloudpath, no_auth, sqlite_filename, progress):
|
|
94
114
|
"""Recursively lists the contents of a directory."""
|
|
95
115
|
cloudpath = normalize_path(cloudpath)
|
|
96
116
|
|
|
@@ -107,6 +127,12 @@ def ls(shortpath, flat, expr, cloudpath, no_auth):
|
|
|
107
127
|
|
|
108
128
|
flat = flat or flt
|
|
109
129
|
|
|
130
|
+
if sqlite_filename != "":
|
|
131
|
+
return listing_db.list(
|
|
132
|
+
cloudpath, sqlite_filename,
|
|
133
|
+
prefix, flat, progress
|
|
134
|
+
)
|
|
135
|
+
|
|
110
136
|
cf = CloudFiles(cloudpath, no_sign_request=no_sign_request)
|
|
111
137
|
iterables = []
|
|
112
138
|
if expr:
|
|
@@ -804,18 +830,19 @@ def __rm(cloudpath, progress, paths):
|
|
|
804
830
|
@click.option('-s', '--summarize', is_flag=True, default=False, help="Sum a total for each input argument.")
|
|
805
831
|
@click.option('-h', '--human-readable', is_flag=True, default=False, help='"Human-readable" output. Use unit suffixes: Bytes, KiB, MiB, GiB, TiB, PiB, and EiB.')
|
|
806
832
|
@click.option('-N', '--count-files', is_flag=True, default=False, help='Also report the number of files.')
|
|
807
|
-
|
|
833
|
+
@click.option('-e', '--group-by-ext', is_flag=True, default=False, help='Group and summarize disk usage by file extension.')
|
|
834
|
+
def du(paths, grand_total, summarize, human_readable, count_files, group_by_ext):
|
|
808
835
|
"""Display disk usage statistics."""
|
|
809
836
|
results = []
|
|
810
837
|
|
|
811
|
-
simplified_data =
|
|
838
|
+
simplified_data = False
|
|
812
839
|
|
|
813
840
|
for path in paths:
|
|
814
841
|
npath = normalize_path(path)
|
|
815
842
|
if ispathdir(path):
|
|
816
843
|
cf = CloudFiles(npath)
|
|
817
|
-
if summarize:
|
|
818
|
-
simplified_data =
|
|
844
|
+
if summarize and not group_by_ext:
|
|
845
|
+
simplified_data = True
|
|
819
846
|
results.append(cf.subtree_size())
|
|
820
847
|
else:
|
|
821
848
|
results.append(cf.size(cf.list()))
|
|
@@ -827,49 +854,43 @@ def du(paths, grand_total, summarize, human_readable, count_files):
|
|
|
827
854
|
return
|
|
828
855
|
results.append({ path: sz })
|
|
829
856
|
|
|
830
|
-
|
|
831
|
-
if not human_readable:
|
|
832
|
-
return val
|
|
833
|
-
|
|
834
|
-
if val < 1024:
|
|
835
|
-
return f"{val} Bytes"
|
|
836
|
-
elif val < 2**20:
|
|
837
|
-
return f"{(val / 2**10):.2f} KiB"
|
|
838
|
-
elif val < 2**30:
|
|
839
|
-
return f"{(val / 2**20):.2f} MiB"
|
|
840
|
-
elif val < 2**40:
|
|
841
|
-
return f"{(val / 2**30):.2f} GiB"
|
|
842
|
-
elif val < 2**50:
|
|
843
|
-
return f"{(val / 2**40):.2f} TiB"
|
|
844
|
-
elif val < 2**60:
|
|
845
|
-
return f"{(val / 2**50):.2f} PiB"
|
|
846
|
-
else:
|
|
847
|
-
return f"{(val / 2**60):.2f} EiB"
|
|
857
|
+
disp = SI if human_readable else (lambda x: x)
|
|
848
858
|
|
|
849
859
|
summary = {}
|
|
850
860
|
num_files = 0
|
|
861
|
+
ext_totals = defaultdict(int)
|
|
862
|
+
|
|
851
863
|
for path, res in zip(paths, results):
|
|
852
864
|
if simplified_data:
|
|
853
|
-
summary[path] = sum(res.values())
|
|
854
|
-
num_files += len(res)
|
|
855
|
-
else:
|
|
856
865
|
summary[path] = res["num_bytes"]
|
|
857
866
|
num_files += res["N"]
|
|
867
|
+
else:
|
|
868
|
+
summary[path] = sum(res.values())
|
|
869
|
+
num_files += len(res)
|
|
870
|
+
if group_by_ext:
|
|
871
|
+
for filepath, size in res.items():
|
|
872
|
+
ext = os.path.splitext(filepath)[1].lower() or "(no extension)"
|
|
873
|
+
ext_totals[ext] += size
|
|
858
874
|
|
|
859
875
|
if summarize:
|
|
860
|
-
print(f"{
|
|
876
|
+
print(f"{disp(summary[path])}\t{path}")
|
|
861
877
|
|
|
862
878
|
if not summarize:
|
|
863
879
|
for res in results:
|
|
864
880
|
for pth, size in res.items():
|
|
865
|
-
print(f"{
|
|
881
|
+
print(f"{disp(size)}\t{pth}")
|
|
866
882
|
|
|
867
883
|
if grand_total:
|
|
868
|
-
print(f"{
|
|
884
|
+
print(f"{disp(sum(summary.values()))}\tbytes total")
|
|
869
885
|
|
|
870
886
|
if count_files:
|
|
871
887
|
print(f"{num_files}\tfiles total")
|
|
872
888
|
|
|
889
|
+
if group_by_ext:
|
|
890
|
+
print("\nUsage by file extension:")
|
|
891
|
+
for ext, total in sorted(ext_totals.items(), key=lambda x: x[1], reverse=True):
|
|
892
|
+
print(f"{disp(total)}\t{ext}")
|
|
893
|
+
|
|
873
894
|
@main.command()
|
|
874
895
|
@click.argument('paths', nargs=-1)
|
|
875
896
|
def head(paths):
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from tqdm import tqdm
|
|
5
|
+
|
|
6
|
+
from cloudfiles.lib import sip
|
|
7
|
+
from cloudfiles import CloudFiles
|
|
8
|
+
|
|
9
|
+
# the maximum value of a host parameter number is
|
|
10
|
+
# SQLITE_MAX_VARIABLE_NUMBER, which defaults to 999
|
|
11
|
+
# for SQLite versions prior to 3.32.0 (2020-05-22) or
|
|
12
|
+
# 32766 for SQLite versions after 3.32.0.
|
|
13
|
+
# https://www.sqlite.org/limits.html
|
|
14
|
+
SQLITE_MAX_PARAMS = 999 if sqlite3.sqlite_version_info <= (3,32,0) else 32766
|
|
15
|
+
BIND = '?'
|
|
16
|
+
|
|
17
|
+
def init_db(filename:str) -> sqlite3.Connection:
|
|
18
|
+
"""Create tables, indexes, and tune SQLite for bulk-insert performance."""
|
|
19
|
+
conn = sqlite3.connect(filename)
|
|
20
|
+
|
|
21
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
22
|
+
conn.execute("PRAGMA synchronous=NORMAL")
|
|
23
|
+
conn.execute("PRAGMA cache_size=-131072") # 128 MB (negative = kilobytes)
|
|
24
|
+
|
|
25
|
+
conn.execute("""
|
|
26
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
27
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
path TEXT NOT NULL UNIQUE,
|
|
29
|
+
size INTEGER
|
|
30
|
+
)
|
|
31
|
+
""")
|
|
32
|
+
|
|
33
|
+
conn.execute("""
|
|
34
|
+
CREATE TABLE IF NOT EXISTS checkpoint (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
page_token TEXT,
|
|
37
|
+
rows_done INTEGER DEFAULT 0,
|
|
38
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
39
|
+
)
|
|
40
|
+
""")
|
|
41
|
+
|
|
42
|
+
conn.execute(
|
|
43
|
+
"""
|
|
44
|
+
INSERT OR IGNORE
|
|
45
|
+
INTO checkpoint (id, page_token, rows_done)
|
|
46
|
+
VALUES (1, "", 0)
|
|
47
|
+
"""
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
conn.execute("""
|
|
51
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
52
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
53
|
+
name TEXT,
|
|
54
|
+
value TEXT
|
|
55
|
+
)
|
|
56
|
+
""")
|
|
57
|
+
|
|
58
|
+
conn.execute(
|
|
59
|
+
"""
|
|
60
|
+
INSERT OR IGNORE
|
|
61
|
+
INTO metadata (id, name, value)
|
|
62
|
+
VALUES (1, "cloudpath", "")
|
|
63
|
+
"""
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
conn.commit()
|
|
67
|
+
|
|
68
|
+
return conn
|
|
69
|
+
|
|
70
|
+
def get_cloudpath(conn:sqlite3.Connection) -> str:
|
|
71
|
+
return conn.execute(
|
|
72
|
+
f"SELECT value FROM metadata where name = 'cloudpath'"
|
|
73
|
+
).fetchone()[0]
|
|
74
|
+
|
|
75
|
+
def save_cloudpath(conn:sqlite3.Connection, cloudpath:str):
|
|
76
|
+
conn.execute(
|
|
77
|
+
f"UPDATE metadata SET value = ? where name = 'cloudpath'", [ cloudpath ]
|
|
78
|
+
)
|
|
79
|
+
conn.commit()
|
|
80
|
+
|
|
81
|
+
def save_checkpoint(
|
|
82
|
+
conn:sqlite3.Connection,
|
|
83
|
+
page_token:str|None,
|
|
84
|
+
rows_done:int,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Persist the current GCS page token and running row count."""
|
|
87
|
+
conn.execute("""
|
|
88
|
+
UPDATE checkpoint
|
|
89
|
+
SET
|
|
90
|
+
page_token = ?,
|
|
91
|
+
rows_done = rows_done + ?
|
|
92
|
+
""", (page_token, rows_done))
|
|
93
|
+
|
|
94
|
+
def load_checkpoint(conn:sqlite3.Connection) -> tuple[str|None,int]:
|
|
95
|
+
"""Return (page_token, rows_done) for a bucket, or (None, 0) if none."""
|
|
96
|
+
row = conn.execute(
|
|
97
|
+
"SELECT page_token, rows_done FROM checkpoint WHERE id = 1"
|
|
98
|
+
).fetchone()
|
|
99
|
+
return (row[0], row[1]) if row else (None, 0)
|
|
100
|
+
|
|
101
|
+
def write_batch(
|
|
102
|
+
conn:sqlite3.Connection,
|
|
103
|
+
batch:list[tuple[str,int]],
|
|
104
|
+
page_token:str|None,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Write a batch of (path, size) rows and save the checkpoint atomically."""
|
|
107
|
+
for rows in sip(batch, SQLITE_MAX_PARAMS):
|
|
108
|
+
bindslist = ",".join([f"({BIND}, {BIND})"] * len(rows))
|
|
109
|
+
flat_rows = [ x for row in rows for x in row ]
|
|
110
|
+
conn.execute(
|
|
111
|
+
f"INSERT INTO files (path,size) VALUES {bindslist}",
|
|
112
|
+
flat_rows
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def list(
|
|
116
|
+
cloudpath:str,
|
|
117
|
+
db_name:str,
|
|
118
|
+
prefix:str = "",
|
|
119
|
+
flat:bool = False,
|
|
120
|
+
progress:bool = False,
|
|
121
|
+
):
|
|
122
|
+
with init_db(db_name) as conn:
|
|
123
|
+
|
|
124
|
+
saved_cloudpath = get_cloudpath(conn)
|
|
125
|
+
if saved_cloudpath not in ("", cloudpath):
|
|
126
|
+
print(
|
|
127
|
+
f"Cloudpath does not match database. Aborting.\n"
|
|
128
|
+
f"Saved: {saved_cloudpath}\n"
|
|
129
|
+
f"Argument: {cloudpath}"
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
cf = CloudFiles(cloudpath)
|
|
134
|
+
|
|
135
|
+
save_cloudpath(conn, cloudpath)
|
|
136
|
+
page_token, rows_done = load_checkpoint(conn)
|
|
137
|
+
|
|
138
|
+
if progress and page_token is not None:
|
|
139
|
+
print(f"Resuming from {rows_done} rows.")
|
|
140
|
+
|
|
141
|
+
iterator = cf.list(
|
|
142
|
+
prefix=prefix,
|
|
143
|
+
flat=flat,
|
|
144
|
+
size=True,
|
|
145
|
+
resume_token=page_token,
|
|
146
|
+
return_resume_token=True,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
pbar = tqdm(
|
|
150
|
+
disable=(not progress),
|
|
151
|
+
desc="Files",
|
|
152
|
+
initial=rows_done,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
with pbar:
|
|
156
|
+
for batch in sip(iterator, 5000):
|
|
157
|
+
latest_token = batch[-1][2]
|
|
158
|
+
batch = [ (row[0], row[1]) for row in batch ]
|
|
159
|
+
conn.execute("BEGIN TRANSACTION")
|
|
160
|
+
write_batch(conn, batch, latest_token)
|
|
161
|
+
save_checkpoint(conn, latest_token, len(batch))
|
|
162
|
+
conn.commit()
|
|
163
|
+
|
|
164
|
+
pbar.update(len(batch))
|
|
165
|
+
|
|
166
|
+
if progress:
|
|
167
|
+
print("creating index.")
|
|
168
|
+
|
|
169
|
+
s = time.monotonic()
|
|
170
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_files_path ON files(path)")
|
|
171
|
+
elapsed = time.monotonic() - s
|
|
172
|
+
if progress:
|
|
173
|
+
print(f"created index in {elapsed:.3f} sec.")
|
|
174
|
+
print("done.")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"git_version": "c6239ff", "is_release": true}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|