# GitHub Issue #1394: Large `cache_time` value to `files.download` causes OverflowError exception

**Issue URL:** https://github.com/pyinfra-dev/pyinfra/issues/1394
**Reported by:** link2xt
**Labels:** bug, operations

## Issue Summary

When using a very large `cache_time` value (e.g., `cache_time=9999999999999`) with the `files.download` operation to effectively disable re-downloading of files, pyinfra crashes with an `OverflowError` on the second run.

### Reproduction

```python
files.download(
    src="https://example.com/file",
    dest="/path/to/file",
    cache_time=9999999999999,  # ~316,887 years
)
```

First run: File downloads successfully
Second run: Crashes with:
```
OverflowError: date value out of range
```

### User's Use Case

The user wanted to use a very large `cache_time` to effectively disable re-downloading of files that should never change. They suggested that if a checksum is provided, the file should not be re-downloaded if the checksum matches - which would eliminate the need for large `cache_time` values.

## Root Cause Analysis

### Location of the Bug

**File:** `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/operations/files.py`
**Line:** 149

```python
ctime = host.get_fact(Date).replace(tzinfo=None) - timedelta(seconds=cache_time)
```

### Why the Error Occurs

1. Python's `datetime` module has minimum and maximum representable dates:
   - `datetime.min` = `0001-01-01 00:00:00`
   - `datetime.max` = `9999-12-31 23:59:59.999999`

2. When `cache_time=9999999999999` (approximately 316,887 years worth of seconds), subtracting this from the current date results in a date before `datetime.min`, causing an `OverflowError`.

3. The calculation `timedelta(seconds=9999999999999)` itself works fine, but when subtracted from a current date (e.g., 2025), the result would be a date around year -314,862, which is impossible to represent.

### Same Issue in apt.py

**File:** `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/operations/apt.py`
**Line:** 322

```python
host_cache_time = host_utc_current_time - timedelta(seconds=cache_time)
```

This has the same potential for `OverflowError` with large `cache_time` values.

## Proposed Fix

### Option 1: Catch OverflowError and Assume Cache Never Expires (Recommended for immediate fix)

Wrap the date subtraction in a try/except block. If an `OverflowError` occurs, assume the cache has not expired (i.e., no download needed).

**For files.py (line 146-151):**

```python
# Current code:
if cache_time:
    # Time on files is not tz-aware, and will be the same tz as the server's time,
    # so we can safely remove the tzinfo from the Date fact before comparison.
    ctime = host.get_fact(Date).replace(tzinfo=None) - timedelta(seconds=cache_time)
    if info["mtime"] and info["mtime"] < ctime:
        download = True

# Proposed fix:
if cache_time:
    # Time on files is not tz-aware, and will be the same tz as the server's time,
    # so we can safely remove the tzinfo from the Date fact before comparison.
    try:
        ctime = host.get_fact(Date).replace(tzinfo=None) - timedelta(seconds=cache_time)
        if info["mtime"] and info["mtime"] < ctime:
            download = True
    except OverflowError:
        # cache_time is so large that the cache effectively never expires
        # (date underflow means the file's mtime will always be "newer" than ctime)
        pass
```

**For apt.py (line 319-325):**

```python
# Current code:
host_utc_current_time = datetime.fromtimestamp(
    host.get_fact(Date).timestamp(), timezone.utc
).replace(tzinfo=None)
host_cache_time = host_utc_current_time - timedelta(seconds=cache_time)
if cache_info["mtime"] > host_cache_time:
    host.noop("apt is already up to date")
    return

# Proposed fix:
host_utc_current_time = datetime.fromtimestamp(
    host.get_fact(Date).timestamp(), timezone.utc
).replace(tzinfo=None)
try:
    host_cache_time = host_utc_current_time - timedelta(seconds=cache_time)
    if cache_info["mtime"] > host_cache_time:
        host.noop("apt is already up to date")
        return
except OverflowError:
    # cache_time is so large that the cache effectively never expires
    host.noop("apt is already up to date")
    return
```

### Option 2: Clamp cache_time to a Maximum Value

Define a maximum `cache_time` value (e.g., 100 years in seconds = ~3,153,600,000) and clamp values to this maximum.

```python
MAX_CACHE_TIME = 3153600000  # ~100 years in seconds

if cache_time:
    effective_cache_time = min(cache_time, MAX_CACHE_TIME)
    ctime = host.get_fact(Date).replace(tzinfo=None) - timedelta(seconds=effective_cache_time)
    # ...
```

### Option 3: Skip cache_time check when checksum is provided (User's Preferred Solution)

The user also suggested that if a checksum is provided and matches, the file should not be re-downloaded regardless of `cache_time`. This is already partially implemented in the current code (lines 153-167), but the logic could be optimized to check checksums before cache_time to avoid this issue entirely:

```python
# Check checksums first - if provided and matching, no need to download
checksum_match = False
if sha1sum and sha1sum == host.get_fact(Sha1File, path=dest):
    checksum_match = True
elif sha256sum and sha256sum == host.get_fact(Sha256File, path=dest):
    checksum_match = True
elif sha384sum and sha384sum == host.get_fact(Sha384File, path=dest):
    checksum_match = True
elif md5sum and md5sum == host.get_fact(Md5File, path=dest):
    checksum_match = True

if checksum_match:
    # File exists with correct checksum, no download needed
    pass
elif cache_time:
    # Check cache expiry
    try:
        ctime = host.get_fact(Date).replace(tzinfo=None) - timedelta(seconds=cache_time)
        if info["mtime"] and info["mtime"] < ctime:
            download = True
    except OverflowError:
        pass
```

## Files Requiring Modification

1. **`/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/operations/files.py`** (line 146-151)
   - Add try/except block around the cache_time calculation

2. **`/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/operations/apt.py`** (line 319-325)
   - Add try/except block around the cache_time calculation

3. **New test case needed:**
   - `tests/operations/files.download/download_cache_time_large.json` - Test with a very large cache_time value to verify no OverflowError occurs

## Test Case Example

```json
{
    "args": ["http://myfile"],
    "kwargs": {
        "dest": "/home/myfile",
        "cache_time": 9999999999999
    },
    "facts": {
        "server.Date": "datetime:2015-01-01T00:30:00",
        "files.File": {
            "path=/home/myfile": {
                "mtime": "datetime:2015-01-01T00:00:00"
            }
        },
        "server.Which": {
            "command=curl": "yes"
        }
    },
    "commands": [],
    "noop_description": "file /home/myfile has already been downloaded"
}
```

## Recommendation

Implement **Option 1** (catch OverflowError) as the primary fix because:

1. It's the simplest and most backwards-compatible solution
2. It correctly interprets the user's intent - a very large cache_time means "never re-download"
3. It doesn't change the API or add new parameters
4. It handles edge cases gracefully without arbitrary limits

Additionally, consider implementing **Option 3** as a separate enhancement to improve the checksum-based workflow, which is a cleaner solution for the use case described by the reporter.
