{"record":{"id":"b6e1f8ac7be4f7ff","repo":"invoke-ai/InvokeAI","slug":"errno-17-file-job-download-path-exists","errorCode":null,"errorMessage":"[Errno 17] File {job.download_path} exists","messagePattern":"\\[Errno 17\\] File (.+?) exists","errorType":"exception","errorClass":"OSError","httpStatus":null,"severity":"error","filePath":"invokeai/app/services/download/download_default.py","lineNumber":571,"sourceCode":"            # Existing file does not match expected size; treat as corrupt and restart.\n            self._logger.debug(\n                \"Resume check: existing file size mismatch; deleting and restarting \"\n                f\"path={job.download_path} existing_size={existing_size} expected={job.total_bytes}\"\n            )\n            job.download_path.unlink()\n\n        free_space = disk_usage(job.download_path.parent).free\n        GB = 2**30\n        remaining_bytes = max(job.total_bytes - job.bytes, 0)\n        if free_space < remaining_bytes:\n            raise RuntimeError(\n                f\"Free disk space {free_space / GB:.2f} GB is not enough for download of {remaining_bytes / GB:.2f} GB.\"\n            )\n\n        # Don't clobber an existing file. See commit 82c2c85202f88c6d24ff84710f297cfc6ae174af\n        # for code that instead resumes an interrupted download.\n        if job.download_path.exists() and resume_from == 0:\n            raise OSError(f\"[Errno 17] File {job.download_path} exists\")\n\n        # append \".downloading\" to the path\n        # signal caller that the download is starting. At this point, key fields such as\n        # download_path and total_bytes will be populated. We call it here because the might\n        # discover that the local file is already complete and generate a COMPLETED status.\n        self._signal_job_started(job)\n\n        expected_total = job.total_bytes or job.expected_total_bytes or content_length\n        # \"range not satisfiable\" - local file is at least as large as the remote file\n        if resp.status_code == 416 or (expected_total > 0 and job.bytes >= expected_total):\n            self._logger.info(f\"{job.download_path}: complete file found. Skipping.\")\n            return\n\n        # \"partial content\" - local file is smaller than remote file\n        elif resp.status_code == 206 or job.bytes > 0:\n            self._logger.info(f\"{job.download_path}: partial file found. Resuming\")\n\n        # some other error","sourceCodeStart":553,"sourceCodeEnd":589,"githubUrl":"https://github.com/invoke-ai/InvokeAI/blob/0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06/invokeai/app/services/download/download_default.py#L553-L589","documentation":"On a fresh (non-resume, resume_from == 0) download, if the target file already exists at job.download_path the library refuses to clobber it and raises OSError mimicking errno 17 (EEXIST). This is deliberate: unlike the earlier size-check block, this branch only triggers when resume logic didn't already reconcile the existing file, protecting an existing complete file from being overwritten.","triggerScenarios":"Submitting a download whose explicit dest file path already exists on disk while resume_from == 0 — e.g. re-submitting the same job with cancel instead of resume, or a leftover file from a prior install.","commonSituations":"Re-running an install script that re-submits already-downloaded models; a prior download crashed leaving the final file in place; two queue entries pointing at the same dest; user copied the file manually before the queue finished.","solutions":["Resume instead of re-submitting: call the queue's resume/join API so the existing file is reconciled (complete files are skipped).","Delete or rename the existing file at job.download_path if you know it is stale, then re-submit.","Check the existing file's size against the remote content-length — if equal, the download is already done; skip it.","Choose a different dest path if the collision is unintended."],"exampleFix":"// before: re-submitting always\nqueue.submit(source, dest)\n\n// after: skip if already present and complete\nif not dest.exists():\n    queue.submit(source, dest)\nelse:\n    logger.info('already downloaded: %s', dest)","handlingStrategy":"validation","validationCode":"import requests\nfrom pathlib import Path\ndef already_downloaded(url: str, dest: Path) -> bool:\n    if not dest.exists():\n        return False\n    remote = int(requests.head(url, allow_redirects=True, timeout=10).headers.get('Content-Length', 0))\n    return remote > 0 and dest.stat().st_size == remote","typeGuard":null,"tryCatchPattern":"try:\n    queue.submit(source, dest)\nexcept OSError as e:\n    if 'Errno 17' in str(e) or 'exists' in str(e):\n        logger.info('%s already present; skipping', dest)","preventionTips":["Check dest.exists() (ideally with size match) before submitting fresh downloads.","Use the queue's resume API for existing files instead of re-submitting.","Make install scripts idempotent: skip submissions whose target file already exists.","Avoid multiple queue entries pointing at the same dest path."],"tags":["filesystem","file-exists","download","oserror"],"backgroundTag":"file-already-exists","analyzedSha":"0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06","analyzedAt":"2026-08-29T04:46:49.967Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}