Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_local_folder.py474 linesDownload Raw Back to huggingface_hub
1# Copyright 2024-present, the HuggingFace Inc. team.2#3# Licensed under the Apache License, Version 2.0 (the "License");4# you may not use this file except in compliance with the License.5# You may obtain a copy of the License at6#7#     http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# distributed under the License is distributed on an "AS IS" BASIS,11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12# See the License for the specific language governing permissions and13# limitations under the License.14"""Contains utilities to handle the `../.cache/huggingface` folder in local directories.15 16First discussed in https://github.com/huggingface/huggingface_hub/issues/1738 to store17download metadata when downloading files from the hub to a local directory (without18using the cache).19 20./.cache/huggingface folder structure:21[4.0K]  data22├── [4.0K]  .cache23│   └── [4.0K]  huggingface24│       └── [4.0K]  download25│           ├── [  16]  file.parquet.metadata26│           ├── [  16]  file.txt.metadata27│           └── [4.0K]  folder28│               └── [  16]  file.parquet.metadata29│30├── [6.5G]  file.parquet31├── [1.5K]  file.txt32└── [4.0K]  folder33    └── [   16]  file.parquet34 35 36Download metadata file structure:37```38# file.txt.metadata3911c5a3d5811f50298f278a704980280950aedb1040a16a55fda99d2f2e7b69cce5cf93ff4ad3049930411712656091.12342 43# file.parquet.metadata4411c5a3d5811f50298f278a704980280950aedb10457c5d3f4b8b76583b422fcb9189ad6c89d5d97a094541ce8932dce3ecabde1421461712656091.12347}48```49"""50 51import base6452import hashlib53import logging54import os55import time56from dataclasses import dataclass57from pathlib import Path58 59from .utils import WeakFileLock60 61 62logger = logging.getLogger(__name__)63 64CACHEDIR_TAG_CONTENT = (65    "Signature: 8a477f597d28d172789f06886806bc55\n"66    "# This file is a cache directory tag created by huggingface_hub.\n"67    "# For information about cache directory tags, see:\n"68    "#\thttps://bford.info/cachedir/\n"69)70 71 72@dataclass73class LocalDownloadFilePaths:74    """75    Paths to the files related to a download process in a local dir.76 77    Returned by [`get_local_download_paths`].78 79    Attributes:80        file_path (`Path`):81            Path where the file will be saved.82        lock_path (`Path`):83            Path to the lock file used to ensure atomicity when reading/writing metadata.84        metadata_path (`Path`):85            Path to the metadata file.86    """87 88    file_path: Path89    lock_path: Path90    metadata_path: Path91 92    def incomplete_path(self, etag: str) -> Path:93        """Return the path where a file will be temporarily downloaded before being moved to `file_path`."""94        path = self.metadata_path.parent / f"{_short_hash(self.metadata_path.name)}.{etag}.incomplete"95        resolved_path = str(path.resolve())96        # Some Windows versions do not allow for paths longer than 255 characters.97        # In this case, we must specify it as an extended path by using the "\\?\" prefix.98        if os.name == "nt" and len(resolved_path) > 255 and not resolved_path.startswith("\\\\?\\"):99            path = Path("\\\\?\\" + resolved_path)100        return path101 102 103@dataclass(frozen=True)104class LocalUploadFilePaths:105    """106    Paths to the files related to an upload process in a local dir.107 108    Returned by [`get_local_upload_paths`].109 110    Attributes:111        path_in_repo (`str`):112            Path of the file in the repo.113        file_path (`Path`):114            Path where the file will be saved.115        lock_path (`Path`):116            Path to the lock file used to ensure atomicity when reading/writing metadata.117        metadata_path (`Path`):118            Path to the metadata file.119    """120 121    path_in_repo: str122    file_path: Path123    lock_path: Path124    metadata_path: Path125 126 127@dataclass128class LocalDownloadFileMetadata:129    """130    Metadata about a file in the local directory related to a download process.131 132    Attributes:133        filename (`str`):134            Path of the file in the repo.135        commit_hash (`str`):136            Commit hash of the file in the repo.137        etag (`str`):138            ETag of the file in the repo. Used to check if the file has changed.139            For LFS files, this is the sha256 of the file. For regular files, it corresponds to the git hash.140        timestamp (`int`):141            Unix timestamp of when the metadata was saved i.e. when the metadata was accurate.142    """143 144    filename: str145    commit_hash: str146    etag: str147    timestamp: float148 149 150@dataclass151class LocalUploadFileMetadata:152    """153    Metadata about a file in the local directory related to an upload process.154    """155 156    size: int157 158    # Default values correspond to "we don't know yet"159    timestamp: float | None = None160    should_ignore: bool | None = None161    sha256: str | None = None162    upload_mode: str | None = None163    remote_oid: str | None = None164    is_uploaded: bool = False165    is_committed: bool = False166 167    def save(self, paths: LocalUploadFilePaths) -> None:168        """Save the metadata to disk."""169        with WeakFileLock(paths.lock_path):170            with paths.metadata_path.open("w") as f:171                new_timestamp = time.time()172                f.write(str(new_timestamp) + "\n")173 174                f.write(str(self.size))  # never None175                f.write("\n")176 177                if self.should_ignore is not None:178                    f.write(str(int(self.should_ignore)))179                f.write("\n")180 181                if self.sha256 is not None:182                    f.write(self.sha256)183                f.write("\n")184 185                if self.upload_mode is not None:186                    f.write(self.upload_mode)187                f.write("\n")188 189                if self.remote_oid is not None:190                    f.write(self.remote_oid)191                f.write("\n")192 193                f.write(str(int(self.is_uploaded)) + "\n")194                f.write(str(int(self.is_committed)) + "\n")195 196            self.timestamp = new_timestamp197 198 199def get_local_download_paths(local_dir: Path, filename: str) -> LocalDownloadFilePaths:200    """Compute paths to the files related to a download process.201 202    Folders containing the paths are all guaranteed to exist.203 204    Args:205        local_dir (`Path`):206            Path to the local directory in which files are downloaded.207        filename (`str`):208            Path of the file in the repo.209 210    Return:211        [`LocalDownloadFilePaths`]: the paths to the files (file_path, lock_path, metadata_path, incomplete_path).212    """213    # filename is the path in the Hub repository (separated by '/')214    # make sure to have a cross-platform transcription215    sanitized_filename = os.path.join(*filename.split("/"))216    if os.name == "nt":217        if sanitized_filename.startswith("..\\") or "\\..\\" in sanitized_filename:218            raise ValueError(219                f"Invalid filename: cannot handle filename '{sanitized_filename}' on Windows. Please ask the repository"220                " owner to rename this file."221            )222    file_path = local_dir / sanitized_filename223    metadata_path = _huggingface_dir(local_dir) / "download" / f"{sanitized_filename}.metadata"224    lock_path = metadata_path.with_suffix(".lock")225 226    # Some Windows versions do not allow for paths longer than 255 characters.227    # In this case, we must specify it as an extended path by using the "\\?\" prefix228    if os.name == "nt":229        if not str(local_dir).startswith("\\\\?\\") and len(os.path.abspath(lock_path)) > 255:230            file_path = Path("\\\\?\\" + os.path.abspath(file_path))231            lock_path = Path("\\\\?\\" + os.path.abspath(lock_path))232            metadata_path = Path("\\\\?\\" + os.path.abspath(metadata_path))233 234    file_path.parent.mkdir(parents=True, exist_ok=True)235    metadata_path.parent.mkdir(parents=True, exist_ok=True)236    return LocalDownloadFilePaths(file_path=file_path, lock_path=lock_path, metadata_path=metadata_path)237 238 239def get_local_upload_paths(local_dir: Path, filename: str) -> LocalUploadFilePaths:240    """Compute paths to the files related to an upload process.241 242    Folders containing the paths are all guaranteed to exist.243 244    Args:245        local_dir (`Path`):246            Path to the local directory that is uploaded.247        filename (`str`):248            Path of the file in the repo.249 250    Return:251        [`LocalUploadFilePaths`]: the paths to the files (file_path, lock_path, metadata_path).252    """253    # filename is the path in the Hub repository (separated by '/')254    # make sure to have a cross-platform transcription255    sanitized_filename = os.path.join(*filename.split("/"))256    if os.name == "nt":257        if sanitized_filename.startswith("..\\") or "\\..\\" in sanitized_filename:258            raise ValueError(259                f"Invalid filename: cannot handle filename '{sanitized_filename}' on Windows. Please ask the repository"260                " owner to rename this file."261            )262    file_path = local_dir / sanitized_filename263    metadata_path = _huggingface_dir(local_dir) / "upload" / f"{sanitized_filename}.metadata"264    lock_path = metadata_path.with_suffix(".lock")265 266    # Some Windows versions do not allow for paths longer than 255 characters.267    # In this case, we must specify it as an extended path by using the "\\?\" prefix268    if os.name == "nt":269        if not str(local_dir).startswith("\\\\?\\") and len(os.path.abspath(lock_path)) > 255:270            file_path = Path("\\\\?\\" + os.path.abspath(file_path))271            lock_path = Path("\\\\?\\" + os.path.abspath(lock_path))272            metadata_path = Path("\\\\?\\" + os.path.abspath(metadata_path))273 274    file_path.parent.mkdir(parents=True, exist_ok=True)275    metadata_path.parent.mkdir(parents=True, exist_ok=True)276    return LocalUploadFilePaths(277        path_in_repo=filename, file_path=file_path, lock_path=lock_path, metadata_path=metadata_path278    )279 280 281def read_download_metadata(local_dir: Path, filename: str) -> LocalDownloadFileMetadata | None:282    """Read metadata about a file in the local directory related to a download process.283 284    Args:285        local_dir (`Path`):286            Path to the local directory in which files are downloaded.287        filename (`str`):288            Path of the file in the repo.289 290    Return:291        `[LocalDownloadFileMetadata]` or `None`: the metadata if it exists, `None` otherwise.292    """293    paths = get_local_download_paths(local_dir, filename)294    with WeakFileLock(paths.lock_path):295        if paths.metadata_path.exists():296            try:297                with paths.metadata_path.open() as f:298                    commit_hash = f.readline().strip()299                    etag = f.readline().strip()300                    timestamp = float(f.readline().strip())301                    metadata = LocalDownloadFileMetadata(302                        filename=filename,303                        commit_hash=commit_hash,304                        etag=etag,305                        timestamp=timestamp,306                    )307            except Exception as e:308                # remove the metadata file if it is corrupted / not the right format309                logger.warning(310                    f"Invalid metadata file {paths.metadata_path}: {e}. Removing it from disk and continue."311                )312                try:313                    paths.metadata_path.unlink()314                except Exception as e:315                    logger.warning(f"Could not remove corrupted metadata file {paths.metadata_path}: {e}")316                return None317 318            try:319                # check if the file exists and hasn't been modified since the metadata was saved320                stat = paths.file_path.stat()321                if (322                    stat.st_mtime - 1 <= metadata.timestamp323                ):  # allow 1s difference as stat.st_mtime might not be precise324                    return metadata325                logger.info(f"Ignored metadata for '{filename}' (outdated). Will re-compute hash.")326            except FileNotFoundError:327                # file does not exist => metadata is outdated328                return None329    return None330 331 332def read_upload_metadata(local_dir: Path, filename: str) -> LocalUploadFileMetadata:333    """Read metadata about a file in the local directory related to an upload process.334 335    TODO: factorize logic with `read_download_metadata`.336 337    Args:338        local_dir (`Path`):339            Path to the local directory in which files are downloaded.340        filename (`str`):341            Path of the file in the repo.342 343    Return:344        `[LocalUploadFileMetadata]` or `None`: the metadata if it exists, `None` otherwise.345    """346    paths = get_local_upload_paths(local_dir, filename)347    with WeakFileLock(paths.lock_path):348        if paths.metadata_path.exists():349            try:350                with paths.metadata_path.open() as f:351                    timestamp = float(f.readline().strip())352 353                    size = int(f.readline().strip())  # never None354 355                    _should_ignore = f.readline().strip()356                    should_ignore = None if _should_ignore == "" else bool(int(_should_ignore))357 358                    _sha256 = f.readline().strip()359                    sha256 = None if _sha256 == "" else _sha256360 361                    _upload_mode = f.readline().strip()362                    upload_mode = None if _upload_mode == "" else _upload_mode363                    if upload_mode not in (None, "regular", "lfs"):364                        raise ValueError(f"Invalid upload mode in metadata {paths.path_in_repo}: {upload_mode}")365 366                    _remote_oid = f.readline().strip()367                    remote_oid = None if _remote_oid == "" else _remote_oid368 369                    is_uploaded = bool(int(f.readline().strip()))370                    is_committed = bool(int(f.readline().strip()))371 372                    metadata = LocalUploadFileMetadata(373                        timestamp=timestamp,374                        size=size,375                        should_ignore=should_ignore,376                        sha256=sha256,377                        upload_mode=upload_mode,378                        remote_oid=remote_oid,379                        is_uploaded=is_uploaded,380                        is_committed=is_committed,381                    )382            except Exception as e:383                # remove the metadata file if it is corrupted / not the right format384                logger.warning(385                    f"Invalid metadata file {paths.metadata_path}: {e}. Removing it from disk and continue."386                )387                try:388                    paths.metadata_path.unlink()389                except Exception as e:390                    logger.warning(f"Could not remove corrupted metadata file {paths.metadata_path}: {e}")391 392                # corrupted metadata => we don't know anything expect its size393                return LocalUploadFileMetadata(size=paths.file_path.stat().st_size)394 395            # TODO: can we do better?396            if (397                metadata.timestamp is not None398                and metadata.is_uploaded  # file was uploaded399                and not metadata.is_committed  # but not committed400                and time.time() - metadata.timestamp > 20 * 3600  # and it's been more than 20 hours401            ):  # => we consider it as garbage-collected by S3402                metadata.is_uploaded = False403 404            # check if the file exists and hasn't been modified since the metadata was saved405            try:406                if metadata.timestamp is not None and paths.file_path.stat().st_mtime <= metadata.timestamp:407                    return metadata408                logger.info(f"Ignored metadata for '{filename}' (outdated). Will re-compute hash.")409            except FileNotFoundError:410                # file does not exist => metadata is outdated411                pass412 413    # empty metadata => we don't know anything expect its size414    return LocalUploadFileMetadata(size=paths.file_path.stat().st_size)415 416 417def write_download_metadata(local_dir: Path, filename: str, commit_hash: str, etag: str) -> None:418    """Write metadata about a file in the local directory related to a download process.419 420    Args:421        local_dir (`Path`):422            Path to the local directory in which files are downloaded.423    """424    paths = get_local_download_paths(local_dir, filename)425    with WeakFileLock(paths.lock_path):426        with paths.metadata_path.open("w") as f:427            f.write(f"{commit_hash}\n{etag}\n{time.time()}\n")428 429 430def _huggingface_dir(local_dir: Path) -> Path:431    """Return the path to the `.cache/huggingface` directory in a local directory."""432    # Wrap in lru_cache to avoid overwriting the .gitignore file if called multiple times433    path = local_dir / ".cache" / "huggingface"434    path.mkdir(exist_ok=True, parents=True)435 436    # Create a CACHEDIR.TAG so backup tools can skip this directory.437    _create_cachedir_tag(path)438 439    # Create a .gitignore file in the .cache/huggingface directory if it doesn't exist440    # Should be thread-safe enough like this.441    gitignore = path / ".gitignore"442    gitignore_lock = path / ".gitignore.lock"443    if not gitignore.exists():444        try:445            with WeakFileLock(gitignore_lock, timeout=0.1):446                gitignore.write_text("*")447        except IndexError:448            pass449        except OSError:  # TimeoutError, FileNotFoundError, PermissionError, etc.450            pass451        try:452            gitignore_lock.unlink()453        except OSError:454            pass455    return path456 457 458def _create_cachedir_tag(cache_dir: Path) -> None:459    """Create a CACHEDIR.TAG file in ``cache_dir`` if one does not already exist.460 461    The tag follows the `Cache Directory Tagging Standard <http://www.brynosaurus.com/cachedir/>`_462    so that backup tools can recognize and skip cache directories.463    """464    tag_path = cache_dir / "CACHEDIR.TAG"465    if not tag_path.exists():466        try:467            tag_path.write_text(CACHEDIR_TAG_CONTENT)468        except OSError:469            pass470 471 472def _short_hash(filename: str) -> str:473    return base64.urlsafe_b64encode(hashlib.sha1(filename.encode()).digest()).decode()474 
codekingpro/portable-devtools · Team Ai