Repository: huggingface/huggingface_hub
Pull request #1045 (2022-09-12 08:55:12)

1025 add time details to scan cache
Add `last_modified` and `last_accessed` info to the cache scanner. Scanned as float values (timestamp) and displayed as "timesince" string (`2 weeks ago`).

This is a preliminary work for the https://github.com/huggingface/huggingface_hub/issues/1025.

Implementation is based on [python's `os.stat` function](https://docs.python.org/3/library/os.html#os.stat) and applied only to the blob files themselves and not the symlinks (that are more platform-dependent).


```text
REPO ID                       REPO TYPE SIZE ON DISK NB FILES LAST_ACCESSED LAST_MODIFIED REFS            LOCAL PATH                                                                   
----------------------------- --------- ------------ -------- ------------- ------------- --------------- ---------------------------------------------------------------------------- 
chrisjay/crowd-speech-africa  dataset         761.7M     4269 4 days ago    4 days ago    main            /home/wauplin/.cache/huggingface/hub/datasets--chrisjay--crowd-speech-africa 
oscar                         dataset           3.3M        3 3 days ago    3 days ago    main            /home/wauplin/.cache/huggingface/hub/datasets--oscar                         
wikiann                       dataset         804.1K      180 1 week ago    1 week ago    main            /home/wauplin/.cache/huggingface/hub/datasets--wikiann                       
z-uo/male-LJSpeech-italian    dataset           5.5G     9409 4 days ago    4 days ago    main            /home/wauplin/.cache/huggingface/hub/datasets--z-uo--male-LJSpeech-italian   
chrisjay/crowd-speech-africa  model             1.2K        1 4 days ago    4 days ago    main            /home/wauplin/.cache/huggingface/hub/models--chrisjay--crowd-speech-africa   
datasets/lhoestq/custom_squad model             4.9M        2 2 weeks ago   2 weeks ago                   /home/wauplin/.cache/huggingface/hub/models--datasets--lhoestq--custom_squad 
gpt2                          model             3.1G       14 1 week ago    1 week ago    main, refs/pr/1 /home/wauplin/.cache/huggingface/hub/models--gpt2                            
osanseviero/flair_test3       model            714.0        1 2 weeks ago   2 weeks ago   main            /home/wauplin/.cache/huggingface/hub/models--osanseviero--flair_test3        

Done in 1.1s. Scanned 8 repo(s) for a total of 9.4G.
Got 2875 warning(s) while scanning. Use -vvv to print details.
```

HuggingFaceDocBuilderDev: The docs for this PR live [here](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1045). All of your documentation changes will be reflected on that endpoint.
codecov[bot]: # [Codecov](https://codecov.io/gh/huggingface/huggingface_hub/pull/1045?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) Report
> Merging [#1045](https://codecov.io/gh/huggingface/huggingface_hub/pull/1045?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) (56c4187) into [main](https://codecov.io/gh/huggingface/huggingface_hub/commit/b9d8617a7989fdaec7adcad95eeedf309dac983c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) (b9d8617) will **increase** coverage by `0.07%`.
> The diff coverage is `93.93%`.

```diff
@@            Coverage Diff             @@
##             main    #1045      +/-   ##
==========================================
+ Coverage   83.69%   83.76%   +0.07%     
==========================================
  Files          37       37              
  Lines        3919     3949      +30     
==========================================
+ Hits         3280     3308      +28     
- Misses        639      641       +2     
```


| [Impacted Files](https://codecov.io/gh/huggingface/huggingface_hub/pull/1045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) | Coverage Δ | |
|---|---|---|
| [src/huggingface\_hub/commands/cache.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/1045/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9jb21tYW5kcy9jYWNoZS5weQ==) | `82.35% <ø> (ø)` | |
| [src/huggingface\_hub/utils/\_cache\_manager.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/1045/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi91dGlscy9fY2FjaGVfbWFuYWdlci5weQ==) | `95.61% <93.93%> (-0.35%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface)

Wauplin: @LysandreJik thanks for the review !

About the nitpick, I agree that it's not optimal. There is another issue (https://github.com/huggingface/huggingface_hub/issues/1024) to add options to this command to have a csv-like output/to select columns. I think it makes sense to address that in a following PR (by default I would also remove the "LOCAL PATH" column that takes far too much space and can be very long)

Patch:
diff --git a/docs/source/how-to-cache.mdx b/docs/source/how-to-cache.mdx
index a0651765..8fff32bd 100644
--- a/docs/source/how-to-cache.mdx
+++ b/docs/source/how-to-cache.mdx
@@ -128,14 +128,14 @@ cached.
 
 ```text
 ➜ huggingface-cli scan-cache
-REPO ID                     REPO TYPE SIZE ON DISK NB FILES REFS                LOCAL PATH
---------------------------- --------- ------------ -------- ------------------- -------------------------------------------------------------------------
-glue                        dataset         116.3K       15 2.4.0, main, 1.17.0 /Users/lucain/.cache/huggingface/hub/datasets--glue
-google/fleurs               dataset          64.9M        6 refs/pr/1, main     /Users/lucain/.cache/huggingface/hub/datasets--google--fleurs
-Jean-Baptiste/camembert-ner model           441.0M        7 main                /Users/lucain/.cache/huggingface/hub/models--Jean-Baptiste--camembert-ner
-bert-base-cased             model             1.9G       13 main                /Users/lucain/.cache/huggingface/hub/models--bert-base-cased
-t5-base                     model            10.1K        3 main                /Users/lucain/.cache/huggingface/hub/models--t5-base
-t5-small                    model           970.7M       11 refs/pr/1, main     /Users/lucain/.cache/huggingface/hub/models--t5-small
+REPO ID                     REPO TYPE SIZE ON DISK NB FILES LAST_ACCESSED LAST_MODIFIED REFS                LOCAL PATH
+--------------------------- --------- ------------ -------- ------------- ------------- ------------------- -------------------------------------------------------------------------
+glue                        dataset         116.3K       15 4 days ago    4 days ago    2.4.0, main, 1.17.0 /home/wauplin/.cache/huggingface/hub/datasets--glue
+google/fleurs               dataset          64.9M        6 1 week ago    1 week ago    refs/pr/1, main     /home/wauplin/.cache/huggingface/hub/datasets--google--fleurs
+Jean-Baptiste/camembert-ner model           441.0M        7 2 weeks ago   16 hours ago  main                /home/wauplin/.cache/huggingface/hub/models--Jean-Baptiste--camembert-ner
+bert-base-cased             model             1.9G       13 1 week ago    2 years ago                       /home/wauplin/.cache/huggingface/hub/models--bert-base-cased
+t5-base                     model            10.1K        3 3 months ago  3 months ago  main                /home/wauplin/.cache/huggingface/hub/models--t5-base
+t5-small                    model           970.7M       11 3 days ago    3 days ago    refs/pr/1, main     /home/wauplin/.cache/huggingface/hub/models--t5-small
 
 Done in 0.0s. Scanned 6 repo(s) for a total of 3.4G.
 Got 1 warning(s) while scanning. Use -vvv to print details.
@@ -150,19 +150,19 @@ usage is only 1.9G.
 
 ```text
 ➜ huggingface-cli scan-cache -v
-REPO ID                     REPO TYPE REVISION                                 SIZE ON DISK NB FILES REFS        LOCAL PATH
---------------------------- --------- ---------------------------------------- ------------ -------- ----------- ----------------------------------------------------------------------------------------------------------------------------
-glue                        dataset   9338f7b671827df886678df2bdd7cc7b4f36dffd        97.7K       14 main, 2.4.0 /Users/lucain/.cache/huggingface/hub/datasets--glue/snapshots/9338f7b671827df886678df2bdd7cc7b4f36dffd
-glue                        dataset   f021ae41c879fcabcf823648ec685e3fead91fe7        97.8K       14 1.17.0      /Users/lucain/.cache/huggingface/hub/datasets--glue/snapshots/f021ae41c879fcabcf823648ec685e3fead91fe7
-google/fleurs               dataset   129b6e96cf1967cd5d2b9b6aec75ce6cce7c89e8        25.4K        3 refs/pr/1   /Users/lucain/.cache/huggingface/hub/datasets--google--fleurs/snapshots/129b6e96cf1967cd5d2b9b6aec75ce6cce7c89e8
-google/fleurs               dataset   24f85a01eb955224ca3946e70050869c56446805        64.9M        4 main        /Users/lucain/.cache/huggingface/hub/datasets--google--fleurs/snapshots/24f85a01eb955224ca3946e70050869c56446805
-Jean-Baptiste/camembert-ner model     dbec8489a1c44ecad9da8a9185115bccabd799fe       441.0M        7 main        /Users/lucain/.cache/huggingface/hub/models--Jean-Baptiste--camembert-ner/snapshots/dbec8489a1c44ecad9da8a9185115bccabd799fe
-bert-base-cased             model     378aa1bda6387fd00e824948ebe3488630ad8565         1.5G        9             /Users/lucain/.cache/huggingface/hub/models--bert-base-cased/snapshots/378aa1bda6387fd00e824948ebe3488630ad8565
-bert-base-cased             model     a8d257ba9925ef39f3036bfc338acf5283c512d9         1.4G        9 main        /Users/lucain/.cache/huggingface/hub/models--bert-base-cased/snapshots/a8d257ba9925ef39f3036bfc338acf5283c512d9
-t5-base                     model     23aa4f41cb7c08d4b05c8f327b22bfa0eb8c7ad9        10.1K        3 main        /Users/lucain/.cache/huggingface/hub/models--t5-base/snapshots/23aa4f41cb7c08d4b05c8f327b22bfa0eb8c7ad9
-t5-small                    model     98ffebbb27340ec1b1abd7c45da12c253ee1882a       726.2M        6 refs/pr/1   /Users/lucain/.cache/huggingface/hub/models--t5-small/snapshots/98ffebbb27340ec1b1abd7c45da12c253ee1882a
-t5-small                    model     d0a119eedb3718e34c648e594394474cf95e0617       485.8M        6             /Users/lucain/.cache/huggingface/hub/models--t5-small/snapshots/d0a119eedb3718e34c648e594394474cf95e0617
-t5-small                    model     d78aea13fa7ecd06c29e3e46195d6341255065d5       970.7M        9 main        /Users/lucain/.cache/huggingface/hub/models--t5-small/snapshots/d78aea13fa7ecd06c29e3e46195d6341255065d5
+REPO ID                     REPO TYPE REVISION                                 SIZE ON DISK NB FILES LAST_MODIFIED REFS        LOCAL PATH
+--------------------------- --------- ---------------------------------------- ------------ -------- ------------- ----------- ----------------------------------------------------------------------------------------------------------------------------
+glue                        dataset   9338f7b671827df886678df2bdd7cc7b4f36dffd        97.7K       14 4 days ago    main, 2.4.0 /home/wauplin/.cache/huggingface/hub/datasets--glue/snapshots/9338f7b671827df886678df2bdd7cc7b4f36dffd
+glue                        dataset   f021ae41c879fcabcf823648ec685e3fead91fe7        97.8K       14 1 week ago    1.17.0      /home/wauplin/.cache/huggingface/hub/datasets--glue/snapshots/f021ae41c879fcabcf823648ec685e3fead91fe7
+google/fleurs               dataset   129b6e96cf1967cd5d2b9b6aec75ce6cce7c89e8        25.4K        3 2 weeks ago   refs/pr/1   /home/wauplin/.cache/huggingface/hub/datasets--google--fleurs/snapshots/129b6e96cf1967cd5d2b9b6aec75ce6cce7c89e8
+google/fleurs               dataset   24f85a01eb955224ca3946e70050869c56446805        64.9M        4 1 week ago    main        /home/wauplin/.cache/huggingface/hub/datasets--google--fleurs/snapshots/24f85a01eb955224ca3946e70050869c56446805
+Jean-Baptiste/camembert-ner model     dbec8489a1c44ecad9da8a9185115bccabd799fe       441.0M        7 16 hours ago  main        /home/wauplin/.cache/huggingface/hub/models--Jean-Baptiste--camembert-ner/snapshots/dbec8489a1c44ecad9da8a9185115bccabd799fe
+bert-base-cased             model     378aa1bda6387fd00e824948ebe3488630ad8565         1.5G        9 2 years ago               /home/wauplin/.cache/huggingface/hub/models--bert-base-cased/snapshots/378aa1bda6387fd00e824948ebe3488630ad8565
+bert-base-cased             model     a8d257ba9925ef39f3036bfc338acf5283c512d9         1.4G        9 3 days ago    main        /home/wauplin/.cache/huggingface/hub/models--bert-base-cased/snapshots/a8d257ba9925ef39f3036bfc338acf5283c512d9
+t5-base                     model     23aa4f41cb7c08d4b05c8f327b22bfa0eb8c7ad9        10.1K        3 1 week ago    main        /home/wauplin/.cache/huggingface/hub/models--t5-base/snapshots/23aa4f41cb7c08d4b05c8f327b22bfa0eb8c7ad9
+t5-small                    model     98ffebbb27340ec1b1abd7c45da12c253ee1882a       726.2M        6 1 week ago    refs/pr/1   /home/wauplin/.cache/huggingface/hub/models--t5-small/snapshots/98ffebbb27340ec1b1abd7c45da12c253ee1882a
+t5-small                    model     d0a119eedb3718e34c648e594394474cf95e0617       485.8M        6 4 weeks ago               /home/wauplin/.cache/huggingface/hub/models--t5-small/snapshots/d0a119eedb3718e34c648e594394474cf95e0617
+t5-small                    model     d78aea13fa7ecd06c29e3e46195d6341255065d5       970.7M        9 1 week ago    main        /home/wauplin/.cache/huggingface/hub/models--t5-small/snapshots/d78aea13fa7ecd06c29e3e46195d6341255065d5
 
 Done in 0.0s. Scanned 6 repo(s) for a total of 3.4G.
 Got 1 warning(s) while scanning. Use -vvv to print details.
@@ -176,9 +176,9 @@ model on a Unix-based machine.
 
 ```text
 ➜ eval "huggingface-cli scan-cache -v" | grep "t5-small"
-t5-small                    model     98ffebbb27340ec1b1abd7c45da12c253ee1882a       726.2M        6 refs/pr/1   /Users/lucain/.cache/huggingface/hub/models--t5-small/snapshots/98ffebbb27340ec1b1abd7c45da12c253ee1882a
-t5-small                    model     d0a119eedb3718e34c648e594394474cf95e0617       485.8M        6             /Users/lucain/.cache/huggingface/hub/models--t5-small/snapshots/d0a119eedb3718e34c648e594394474cf95e0617
-t5-small                    model     d78aea13fa7ecd06c29e3e46195d6341255065d5       970.7M        9 main        /Users/lucain/.cache/huggingface/hub/models--t5-small/snapshots/d78aea13fa7ecd06c29e3e46195d6341255065d5
+t5-small                    model     98ffebbb27340ec1b1abd7c45da12c253ee1882a       726.2M        6 1 week ago    refs/pr/1   /home/wauplin/.cache/huggingface/hub/models--t5-small/snapshots/98ffebbb27340ec1b1abd7c45da12c253ee1882a
+t5-small                    model     d0a119eedb3718e34c648e594394474cf95e0617       485.8M        6 4 weeks ago               /home/wauplin/.cache/huggingface/hub/models--t5-small/snapshots/d0a119eedb3718e34c648e594394474cf95e0617
+t5-small                    model     d78aea13fa7ecd06c29e3e46195d6341255065d5       970.7M        9 1 week ago    main        /home/wauplin/.cache/huggingface/hub/models--t5-small/snapshots/d78aea13fa7ecd06c29e3e46195d6341255065d5
 ```
 
 ### From Python
@@ -208,17 +208,23 @@ HFCacheInfo(
             repo_path=PosixPath(...),
             size_on_disk=970726914,
             nb_files=11,
+            last_accessed=1662971707.3567169,
+            last_modified=1662971107.3567169,
             revisions=frozenset({
                 CachedRevisionInfo(
                     commit_hash='d78aea13fa7ecd06c29e3e46195d6341255065d5',
                     size_on_disk=970726339,
                     snapshot_path=PosixPath(...),
+                    # No `last_accessed` as blobs are shared among revisions
+                    last_modified=1662971107.3567169,
                     files=frozenset({
                         CachedFileInfo(
                             file_name='config.json',
                             size_on_disk=1197
                             file_path=PosixPath(...),
                             blob_path=PosixPath(...),
+                            blob_last_accessed=1662971707.3567169,
+                            blob_last_modified=1662971107.3567169,
                         ),
                         CachedFileInfo(...),
                         ...
diff --git a/src/huggingface_hub/commands/cache.py b/src/huggingface_hub/commands/cache.py
index 7c0f9d09..16dd59cd 100644
--- a/src/huggingface_hub/commands/cache.py
+++ b/src/huggingface_hub/commands/cache.py
@@ -81,6 +81,8 @@ class ScanCacheCommand(BaseHuggingfaceCLICommand):
                             repo.repo_type,
                             "{:>12}".format(repo.size_on_disk_str),
                             repo.nb_files,
+                            repo.last_accessed_str,
+                            repo.last_modified_str,
                             ", ".join(sorted(repo.refs)),
                             str(repo.repo_path),
                         ]
@@ -93,6 +95,8 @@ class ScanCacheCommand(BaseHuggingfaceCLICommand):
                         "REPO TYPE",
                         "SIZE ON DISK",
                         "NB FILES",
+                        "LAST_ACCESSED",
+                        "LAST_MODIFIED",
                         "REFS",
                         "LOCAL PATH",
                     ],
@@ -108,6 +112,7 @@ class ScanCacheCommand(BaseHuggingfaceCLICommand):
                             revision.commit_hash,
                             "{:>12}".format(revision.size_on_disk_str),
                             revision.nb_files,
+                            revision.last_modified_str,
                             ", ".join(sorted(revision.refs)),
                             str(revision.snapshot_path),
                         ]
@@ -124,6 +129,7 @@ class ScanCacheCommand(BaseHuggingfaceCLICommand):
                         "REVISION",
                         "SIZE ON DISK",
                         "NB FILES",
+                        "LAST_MODIFIED",
                         "REFS",
                         "LOCAL PATH",
                     ],
diff --git a/src/huggingface_hub/utils/_cache_manager.py b/src/huggingface_hub/utils/_cache_manager.py
index 58f15b33..a1c7d5f3 100644
--- a/src/huggingface_hub/utils/_cache_manager.py
+++ b/src/huggingface_hub/utils/_cache_manager.py
@@ -15,6 +15,7 @@
 """Contains utilities to manage the HF cache directory."""
 import os
 import shutil
+import time
 from collections import defaultdict
 from dataclasses import dataclass
 from pathlib import Path
@@ -48,6 +49,19 @@ class CachedFileInfo:
             Path of the blob file. This is equivalent to `file_path.resolve()`.
         size_on_disk (`int`):
             Size of the blob file in bytes.
+        blob_last_accessed (`float`):
+            Timestamp of the last time the blob file has been accessed (from any
+            revision).
+        blob_last_modified (`float`):
+            Timestamp of the last time the blob file has been modified/created.
+
+    <Tip warning={true}>
+
+    `blob_last_accessed` and `blob_last_modified` reliability can depend on the OS you
+    are using. See [python documentation](https://docs.python.org/3/library/os.html#os.stat_result)
+    for more details.
+
+    </Tip>
     """
 
     file_name: str
@@ -55,6 +69,29 @@ class CachedFileInfo:
     blob_path: Path
     size_on_disk: int
 
+    blob_last_accessed: float
+    blob_last_modified: float
+
+    @property
+    def blob_last_accessed_str(self) -> str:
+        """
+        (property) Timestamp of the last time the blob file has been accessed (from any
+        revision), returned as a human-readable string.
+
+        Example: "2 weeks ago".
+        """
+        return _format_timesince(self.blob_last_accessed)
+
+    @property
+    def blob_last_modified_str(self) -> str:
+        """
+        (property) Timestamp of the last time the blob file has been modified, returned
+        as a human-readable string.
+
+        Example: "2 weeks ago".
+        """
+        return _format_timesince(self.blob_last_modified)
+
     @property
     def size_on_disk_str(self) -> str:
         """
@@ -80,14 +117,23 @@ class CachedRevisionInfo:
         snapshot_path (`Path`):
             Path to the revision directory in the `snapshots` folder. It contains the
             exact tree structure as the repo on the Hub.
-        size_on_disk (`int`):
-            Sum of the blob file sizes that are symlink-ed by the revision.
         files: (`FrozenSet[CachedFileInfo]`):
             Set of [`~CachedFileInfo`] describing all files contained in the snapshot.
         refs (`FrozenSet[str]`):
             Set of `refs` pointing to this revision. If the revision has no `refs`, it
             is considered detached.
             Example: `{"main", "2.4.0"}` or `{"refs/pr/1"}`.
+        size_on_disk (`int`):
+            Sum of the blob file sizes that are symlink-ed by the revision.
+        last_modified (`float`):
+            Timestamp of the last time the revision has been created/modified.
+
+    <Tip warning={true}>
+
+    `last_accessed` cannot be determined correctly on a single revision as blob files
+    are shared across revisions.
+
+    </Tip>
 
     <Tip warning={true}>
 
@@ -104,6 +150,18 @@ class CachedRevisionInfo:
     files: FrozenSet[CachedFileInfo]
     refs: FrozenSet[str]
 
+    last_modified: float
+
+    @property
+    def last_modified_str(self) -> str:
+        """
+        (property) Timestamp of the last time the revision has been modified, returned
+        as a human-readable string.
+
+        Example: "2 weeks ago".
+        """
+        return _format_timesince(self.last_modified)
+
     @property
     def size_on_disk_str(self) -> str:
         """
@@ -138,6 +196,10 @@ class CachedRepoInfo:
             Total number of blob files in the cached repo.
         revisions (`FrozenSet[CachedRevisionInfo]`):
             Set of [`~CachedRevisionInfo`] describing all revisions cached in the repo.
+        last_accessed (`float`):
+            Timestamp of the last time a blob file of the repo has been accessed.
+        last_modified (`float`):
+            Timestamp of the last time a blob file of the repo has been modified/created.
 
     <Tip warning={true}>
 
@@ -145,6 +207,14 @@ class CachedRepoInfo:
     duplicated files. Besides, only blobs are taken into account, not the (negligible)
     size of folders and symlinks.
 
+    </Tip>
+
+    <Tip warning={true}>
+
+    `last_accessed` and `last_modified` reliability can depend on the OS you are using.
+    See [python documentation](https://docs.python.org/3/library/os.html#os.stat_result)
+    for more details.
+
     </Tip>
     """
 
@@ -155,6 +225,29 @@ class CachedRepoInfo:
     nb_files: int
     revisions: FrozenSet[CachedRevisionInfo]
 
+    last_accessed: float
+    last_modified: float
+
+    @property
+    def last_accessed_str(self) -> str:
+        """
+        (property) Last time a blob file of the repo has been accessed, returned as a
+        human-readable string.
+
+        Example: "2 weeks ago".
+        """
+        return _format_timesince(self.last_accessed)
+
+    @property
+    def last_modified_str(self) -> str:
+        """
+        (property) Last time a blob file of the repo has been modified, returned as a
+        human-readable string.
+
+        Example: "2 weeks ago".
+        """
+        return _format_timesince(self.last_modified)
+
     @property
     def size_on_disk_str(self) -> str:
         """
@@ -534,7 +627,7 @@ def _scan_cached_repo(repo_path: Path) -> CachedRepoInfo:
             f" ({repo_path})."
         )
 
-    blob_sizes: Dict[Path, int] = {}  # Key is blob_path, value is blob size (in bytes)
+    blob_stats: Dict[Path, os.stat_result] = {}  # Key is blob_path, value is blob stats
 
     snapshots_path = repo_path / "snapshots"
     refs_path = repo_path / "refs"
@@ -602,28 +695,33 @@ def _scan_cached_repo(repo_path: Path) -> CachedRepoInfo:
                     f"Blob symlink points outside of blob directory: {blob_path}"
                 )
 
-            if blob_path not in blob_sizes:
-                blob_sizes[blob_path] = blob_path.stat().st_size
+            if blob_path not in blob_stats:
+                blob_stats[blob_path] = blob_path.stat()
 
             cached_files.add(
                 CachedFileInfo(
                     file_name=file_path.name,
                     file_path=file_path,
-                    size_on_disk=blob_sizes[blob_path],
+                    size_on_disk=blob_stats[blob_path].st_size,
                     blob_path=blob_path,
+                    blob_last_accessed=blob_stats[blob_path].st_atime,
+                    blob_last_modified=blob_stats[blob_path].st_mtime,
                 )
             )
 
         cached_revisions.add(
             CachedRevisionInfo(
                 commit_hash=revision_path.name,
+                files=frozenset(cached_files),
+                refs=frozenset(refs_by_hash.pop(revision_path.name, set())),
                 size_on_disk=sum(
-                    blob_sizes[blob_path]
+                    blob_stats[blob_path].st_size
                     for blob_path in set(file.blob_path for file in cached_files)
                 ),
-                files=frozenset(cached_files),
                 snapshot_path=revision_path,
-                refs=frozenset(refs_by_hash.pop(revision_path.name, set())),
+                last_modified=max(
+                    blob_stats[file.blob_path].st_mtime for file in cached_files
+                ),
             )
         )
 
@@ -636,12 +734,14 @@ def _scan_cached_repo(repo_path: Path) -> CachedRepoInfo:
 
     # Build and return frozen structure
     return CachedRepoInfo(
+        nb_files=len(blob_stats),
         repo_id=repo_id,
+        repo_path=repo_path,
         repo_type=repo_type,  # type: ignore
         revisions=frozenset(cached_revisions),
-        repo_path=repo_path,
-        size_on_disk=sum(blob_sizes.values()),
-        nb_files=len(blob_sizes),
+        size_on_disk=sum(stat.st_size for stat in blob_stats.values()),
+        last_accessed=max(stat.st_atime for stat in blob_stats.values()),
+        last_modified=max(stat.st_mtime for stat in blob_stats.values()),
     )
 
 
@@ -658,6 +758,32 @@ def _format_size(num: int) -> str:
     return f"{num_f:.1f}Y"
 
 
+_TIMESINCE_CHUNKS = (
+    # Label, divider, max value
+    ("second", 1, 60),
+    ("minute", 60, 60),
+    ("hour", 60 * 60, 24),
+    ("day", 60 * 60 * 24, 6),
+    ("week", 60 * 60 * 24 * 7, 6),
+    ("month", 60 * 60 * 24 * 30, 11),
+    ("year", 60 * 60 * 24 * 365, None),
+)
+
+
+def _format_timesince(ts: float) -> str:
+    """Format timestamp in seconds into a human-readable string, relative to now.
+
+    Vaguely inspired by Django's `timesince` formatter.
+    """
+    delta = time.time() - ts
+    if delta < 20:
+        return "a few seconds ago"
+    for label, divider, max_value in _TIMESINCE_CHUNKS:
+        value = round(delta / divider)
+        if max_value is None or value <= max_value:
+            return f"{value} {label}{'s' if value > 1 else ''} ago"
+
+
 def _try_delete_path(path: Path, path_type: str) -> None:
     """Try to delete a local file or folder.
 


Test patch:
diff --git a/tests/test_utils_cache.py b/tests/test_utils_cache.py
index d1110916..9d8c25ad 100644
--- a/tests/test_utils_cache.py
+++ b/tests/test_utils_cache.py
@@ -1,6 +1,7 @@
 import os
 import shutil
 import sys
+import time
 import unittest
 from io import StringIO
 from pathlib import Path
@@ -14,7 +15,11 @@ from _pytest.fixtures import SubRequest
 from huggingface_hub._snapshot_download import snapshot_download
 from huggingface_hub.commands.cache import ScanCacheCommand
 from huggingface_hub.utils import DeleteCacheStrategy, HFCacheInfo, scan_cache_dir
-from huggingface_hub.utils._cache_manager import _try_delete_path
+from huggingface_hub.utils._cache_manager import (
+    _format_size,
+    _format_timesince,
+    _try_delete_path,
+)
 
 from .testing_constants import TOKEN
 
@@ -196,10 +201,10 @@ class TestValidCacheUtils(unittest.TestCase):
         sys.stdout = previous_output
 
         expected_output = f"""
-        REPO ID                       REPO TYPE SIZE ON DISK NB FILES REFS            LOCAL PATH
-        ----------------------------- --------- ------------ -------- --------------- -------------------------------------------------------------------------------------------------------------
-        valid_org/test_scan_dataset_b dataset           2.2K        2 main            {self.cache_dir}/datasets--valid_org--test_scan_dataset_b
-        valid_org/test_scan_repo_a    model             1.4K        4 main, refs/pr/1 {self.cache_dir}/models--valid_org--test_scan_repo_a
+        REPO ID                       REPO TYPE SIZE ON DISK NB FILES LAST_ACCESSED     LAST_MODIFIED     REFS            LOCAL PATH
+        ----------------------------- --------- ------------ -------- ----------------- ----------------- --------------- ---------------------------------------------------------
+        valid_org/test_scan_dataset_b dataset           2.2K        2 a few seconds ago a few seconds ago main            {self.cache_dir}/datasets--valid_org--test_scan_dataset_b
+        valid_org/test_scan_repo_a    model             1.4K        4 a few seconds ago a few seconds ago main, refs/pr/1 {self.cache_dir}/models--valid_org--test_scan_repo_a
 
         Done in 0.0s. Scanned 2 repo(s) for a total of \x1b[1m\x1b[31m3.5K\x1b[0m.
         """
@@ -226,12 +231,12 @@ class TestValidCacheUtils(unittest.TestCase):
         sys.stdout = previous_output
 
         expected_output = f"""
-        REPO ID                       REPO TYPE REVISION                                 SIZE ON DISK NB FILES REFS      LOCAL PATH
-        ----------------------------- --------- ---------------------------------------- ------------ -------- --------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------
-        valid_org/test_scan_dataset_b dataset   1ac47c6f707cbc4825c2aa431ad5ab8cf09e60ed         2.2K        2 main      {self.cache_dir}/datasets--valid_org--test_scan_dataset_b/snapshots/1ac47c6f707cbc4825c2aa431ad5ab8cf09e60ed
-        valid_org/test_scan_repo_a    model     1da18ebd9185d146bcf84e308de53715d97d67d1         1.3K        1           {self.cache_dir}/models--valid_org--test_scan_repo_a/snapshots/1da18ebd9185d146bcf84e308de53715d97d67d1
-        valid_org/test_scan_repo_a    model     401874e6a9c254a8baae85edd8a073921ecbd7f5         1.4K        3 main      {self.cache_dir}/models--valid_org--test_scan_repo_a/snapshots/401874e6a9c254a8baae85edd8a073921ecbd7f5
-        valid_org/test_scan_repo_a    model     fc674b0d440d3ea6f94bc4012e33ebd1dfc11b5b         1.4K        4 refs/pr/1 {self.cache_dir}/models--valid_org--test_scan_repo_a/snapshots/fc674b0d440d3ea6f94bc4012e33ebd1dfc11b5b
+        REPO ID                       REPO TYPE REVISION                                 SIZE ON DISK NB FILES LAST_MODIFIED     REFS      LOCAL PATH
+        ----------------------------- --------- ---------------------------------------- ------------ -------- ----------------- --------- ------------------------------------------------------------------------------------------------------------
+        valid_org/test_scan_dataset_b dataset   1ac47c6f707cbc4825c2aa431ad5ab8cf09e60ed         2.2K        2 a few seconds ago main      {self.cache_dir}/datasets--valid_org--test_scan_dataset_b/snapshots/1ac47c6f707cbc4825c2aa431ad5ab8cf09e60ed
+        valid_org/test_scan_repo_a    model     1da18ebd9185d146bcf84e308de53715d97d67d1         1.3K        1 a few seconds ago           {self.cache_dir}/models--valid_org--test_scan_repo_a/snapshots/1da18ebd9185d146bcf84e308de53715d97d67d1
+        valid_org/test_scan_repo_a    model     401874e6a9c254a8baae85edd8a073921ecbd7f5         1.4K        3 a few seconds ago main      {self.cache_dir}/models--valid_org--test_scan_repo_a/snapshots/401874e6a9c254a8baae85edd8a073921ecbd7f5
+        valid_org/test_scan_repo_a    model     fc674b0d440d3ea6f94bc4012e33ebd1dfc11b5b         1.4K        4 a few seconds ago refs/pr/1 {self.cache_dir}/models--valid_org--test_scan_repo_a/snapshots/fc674b0d440d3ea6f94bc4012e33ebd1dfc11b5b
 
         Done in 0.0s. Scanned 2 repo(s) for a total of \x1b[1m\x1b[31m3.5K\x1b[0m.
         """
@@ -248,7 +253,7 @@ class TestCorruptedCacheUtils(unittest.TestCase):
     repo_path: Path
 
     def setUp(self) -> None:
-        """Setup a clean cache for tests that will get corrupted in tests."""
+        """Setup a clean cache for tests that will get corrupted/modified in tests."""
         # Download latest main
         snapshot_download(
             repo_id=VALID_MODEL_ID,
@@ -347,6 +352,77 @@ class TestCorruptedCacheUtils(unittest.TestCase):
             + f"({self.repo_path }).",
         )
 
+    def test_scan_cache_last_modified_and_last_accessed(self) -> None:
+        """Scan the last_modified and last_accessed properties when scanning."""
+        TIME_GAP = 0.1
+
+        # Make a first scan
+        report_1 = scan_cache_dir(self.cache_dir)
+
+        # Values from first report
+        repo_1 = list(report_1.repos)[0]
+        revision_1 = list(repo_1.revisions)[0]
+        readme_file_1 = [
+            file for file in revision_1.files if file.file_name == "README.md"
+        ][0]
+        another_file_1 = [
+            file for file in revision_1.files if file.file_name == "Another file.md"
+        ][0]
+
+        # Comparison of last_accessed/last_modified between file and repo
+        self.assertLessEqual(readme_file_1.blob_last_accessed, repo_1.last_accessed)
+        self.assertLessEqual(readme_file_1.blob_last_modified, repo_1.last_modified)
+        self.assertEqual(revision_1.last_modified, repo_1.last_modified)
+
+        # Sleep and write new readme
+        time.sleep(TIME_GAP)
+        readme_file_1.file_path.write_text("modified readme")
+
+        # Sleep and read content from readme
+        time.sleep(TIME_GAP)
+        with readme_file_1.file_path.open("r") as f:
+            _ = f.read()
+
+        # Sleep and re-scan
+        time.sleep(TIME_GAP)
+        report_2 = scan_cache_dir(self.cache_dir)
+
+        # Values from second report
+        repo_2 = list(report_2.repos)[0]
+        revision_2 = list(repo_2.revisions)[0]
+        readme_file_2 = [
+            file for file in revision_2.files if file.file_name == "README.md"
+        ][0]
+        another_file_2 = [
+            file for file in revision_1.files if file.file_name == "Another file.md"
+        ][0]
+
+        # Report 1 is not updated when cache changes
+        self.assertLess(repo_1.last_accessed, repo_2.last_accessed)
+        self.assertLess(repo_1.last_modified, repo_2.last_modified)
+
+        # "Another_file.md" did not change
+        self.assertEqual(another_file_1, another_file_2)
+
+        # Readme.md has been modified and then accessed more recently
+        self.assertGreaterEqual(
+            readme_file_2.blob_last_modified - readme_file_1.blob_last_modified,
+            TIME_GAP * 0.999,  # 0.999 factor because not exactly precise
+        )
+        self.assertGreaterEqual(
+            readme_file_2.blob_last_accessed - readme_file_1.blob_last_accessed,
+            2 * TIME_GAP * 0.999,  # 0.999 factor because not exactly precise
+        )
+        self.assertGreaterEqual(
+            readme_file_2.blob_last_accessed - readme_file_2.blob_last_modified,
+            TIME_GAP * 0.999,  # 0.999 factor because not exactly precise
+        )
+
+        # Comparison of last_accessed/last_modified between file and repo
+        self.assertEqual(readme_file_2.blob_last_accessed, repo_2.last_accessed)
+        self.assertEqual(readme_file_2.blob_last_modified, repo_2.last_modified)
+        self.assertEqual(revision_2.last_modified, repo_2.last_modified)
+
 
 class TestDeleteRevisionsDryRun(unittest.TestCase):
     cache_info: Mock  # Mocked HFCacheInfo
@@ -649,3 +725,43 @@ class TestTryDeletePath(unittest.TestCase):
 
         # For proper cleanup
         dir_path.chmod(509)
+
+
+class TestStringFormatters(unittest.TestCase):
+    SIZES = {
+        16.0: "16.0",
+        1000.0: "1.0K",
+        1024 * 1024 * 1024: "1.1G",  # not 1.0GiB
+    }
+
+    SINCE = {
+        1: "a few seconds ago",
+        15: "a few seconds ago",
+        25: "25 seconds ago",
+        80: "1 minute ago",
+        1000: "17 minutes ago",
+        4000: "1 hour ago",
+        8000: "2 hours ago",
+        3600 * 24 * 13: "2 weeks ago",
+        3600 * 24 * 30 * 8.2: "8 months ago",
+        3600 * 24 * 365: "1 year ago",
+        3600 * 24 * 365 * 9.6: "10 years ago",
+    }
+
+    def test_format_size(self) -> None:
+        """Test `_format_size` formatter."""
+        for size, expected in self.SIZES.items():
+            self.assertEqual(
+                _format_size(size),
+                expected,
+                msg=f"Wrong formatting for {size} == '{expected}'",
+            )
+
+    def test_format_timesince(self) -> None:
+        """Test `_format_timesince` formatter."""
+        for ts, expected in self.SINCE.items():
+            self.assertEqual(
+                _format_timesince(time.time() - ts),
+                expected,
+                msg=f"Wrong formatting for {ts} == '{expected}'",
+            )