Guides

Bulk import

Move images from an old image host or a blog into ImgYoung in bulk, from a list of image URLs, a sitemap, a feed, a web page, or Markdown and HTML text, and export a mapping from old links to new ones.

When to use it

An old image host is shutting down, or the images on your blog are scattered across several places, and you want them all in ImgYoung together with an "old link → new link" mapping for updating your articles. Bulk import does exactly that:

  1. Give it image addresses, or a site or text where images can be found.
  2. Confirm which domains to import from.
  3. The server fetches and stores the images one by one; when it finishes, download the mapping.

Imported images belong to the signed-in account and go through the same processing as an upload: the format allowlist, pixel budget, EXIF handling and storage quota all apply. To fetch a single image, use fetching from a URL.

Every signed-in user can use it. Regular users see and manage only the jobs they created; administrators see everyone's jobs and can cancel or delete any of them, but only the creator can start or retry a job. The whole instance processes one import job at a time, so an administrator can cancel a stuck job to let the ones behind it continue.

Creating an import

In the Content group of the sidebar open Imports and click New import; you can also click Bulk import next to Upload on the Images page. The dialog has three tabs; pick the one that matches what you have:

TabWhat to enterThen
Image URLsImage addresses, one per lineThe import starts right away
WebsiteSitemap, feed or web page addresses, one per lineAfter scanning, confirm the domains to import
Text and filesPasted Markdown or HTML text, or filesAfter scanning, confirm the domains to import

Image URLs

One http or https image address per line. An address that appears more than once is imported once. The job goes straight to Importing, with no confirmation step.

Website

One address per line. What each address is gets decided from the content fetched, not from its extension:

The address points toWhat happens
A sitemap (urlset, sitemapindex, gzip included)Every article page in it is scanned; sub-sitemaps in an index are expanded in turn
A feed (RSS, Atom)Every article page in it is scanned
Plain text with one address per lineTreated as a text sitemap, each page scanned
A web pageThat page is scanned
An imageImported directly

The server expands only as far as article pages and never follows links found inside pages. Pages in a sitemap or feed that do not belong to its own domain are skipped; a web page you enter directly is not subject to this rule.

The job starts as Scanning and shows how many pages have been processed. When the scan ends it becomes Ready to confirm.

Text and files

Paste text, or pick one or more .md, .markdown, .html, .htm or .txt files. Pasted text appears in the report as "Pasted text"; a file is named by its file name.

If the text uses relative image paths, enter the address of the page the article is published at as the Base URL, and the server resolves them against it. Without a base URL, relative paths cannot be fetched and are listed under unfetchable references in the scan report. A <base href> in an HTML document takes precedence over the base URL.

Scanning finishes while you submit, and the job goes straight to Ready to confirm.

Advanced options

Shared by all three tabs and all optional:

OptionEffect
ExpiryThe lifetime of the imported images. When left empty, your default lifetime applies, then the global default; see Lifecycle and deduplication
EXIFStrip or keep metadata. When left empty, the global default applies; see EXIF handling
RefererThe source page address sent when fetching images, for old hosts with hotlink protection; see Hosts with hotlink protection

Confirming the domains to import

Open a job that is Ready to confirm and the page shows:

  • Scan report: how many pages or documents were scanned, how many had no images, how many failed to load (each listed with its address and reason; import_listing_invalid when the content of a sitemap or feed cannot be parsed), how many image references could not be fetched (each listed), and how many pages were skipped because they are outside the site's domain.
  • Domain table: the images found, grouped by domain, one row per domain with the image count and up to 3 sample addresses, largest first.

No domain is selected by default. Tick the domains of the old image host and click Start import: images under the selected domains are imported and the rest are not. Select all and Clear work in bulk. The table shows address text only and loads no thumbnails.

While no domain is ticked, Start import is disabled. If the scan found no images at all, the page says "No images found" and the only thing you can do is delete the job.

A single page often has images from several domains: the old host, analytics icons and third-party avatars all show up in the table. Tick only the one you want to move.

While the import runs

Once the job is Importing, the page shows progress and the count in each state, and failed items appear as they happen; there is no need to refresh.

  • Concurrency: a job transfers 4 images at a time. These 4 slots are independent of the upload concurrency limit and do not use the upload rate allowance; when planning memory, count the two together.
  • Queueing: the whole instance processes one import job at a time. The other jobs wait in the order they were created and show no progress meanwhile.
  • Processing: each image goes through the same steps as an upload and, once stored, records the new short ID. The file name is the last segment of the address path.
  • Failures: one failed image does not affect the others; the reason is recorded on the item. When every item has been processed the job becomes Done, even if some items failed.
  • Restarts: after a restart, an Importing job continues from the items not yet processed, and images already stored are not imported again; a Scanning job is scanned again from the start.

Failed items

Error codeUsual causeWhat to do
url_blockedThe address is not a public addressPrivate addresses cannot be imported; see Fetch restrictions
url_too_largeThe image exceeds the per-image size limitRaise Max image size, then retry
url_bad_statusThe remote server returned a non-2xx status, often hotlink protectionEnter a Referer and create the job again, or check whether the old address still exists
url_fetch_failedTimeout or connection failureRetry later
url_too_many_redirectsMore than 5 redirectsCheck the address
upload_unsupported_typeThe format is not on the allowlist, such as SVG or AVIFCannot be imported; see Formats and limits
upload_invalid_imageThe content is not a decodable imageCheck that the address really points to an image
upload_too_many_pixelsThe resolution exceeds the pixel budgetRaise Pixel budget, then retry

The page shows a localized explanation for each code, plus the original message from the remote server or the server (at most 500 characters). Each image gets at most 20 minutes.

Retry

When a job has ended, click Retry: failed items and unprocessed items go back to Importing together. Items that already succeeded are not affected. Failed items are never retried automatically.

A Done, Aborted or Cancelled job can be retried; if it has no failed or unprocessed items, the retry is refused (import_nothing_to_retry). A website job whose scan did not finish before it was aborted or cancelled has no candidates to continue from, so retrying scans it again from the start.

Aborting

When no channel is available or the server fails (for example the database), carrying on would only make every remaining image fail the same way, so the whole job stops as Aborted. The page shows the reason and the unprocessed items are kept. The storage quota works the same way: when 3 images in a row fail because of the quota, the quota is considered full and the job stops. If just one image is larger than the remaining quota, only that image fails and the images that still fit are imported. Fix the problem first (raise the quota, repair the channel), then click Retry to continue.

Cancelling

Jobs that are Scanning, Ready to confirm or Importing can be cancelled. Images being transferred are interrupted and their items stay pending; images already imported are kept. A cancelled job can be retried.

Mapping

Once a job has ended (Done, Aborted or Cancelled), click Download mapping and choose JSON or CSV. The mapping contains only items that were imported successfully and whose image still exists. The new address is the image's full access address: the public site address (base_url) when it is set, otherwise generated from the domain you are using.

ColumnMeaning
old_urlThe address of the old image
new_urlThe address of the new image
image_idThe new image's short ID
sourceThe page address or document name that references this image
rawThe reference as written in that place, possibly a relative path; use it to find and replace in the source article
  • CSV: UTF-8 with a header row and one row per reference; an image with no reference gets one row with source and raw empty. Cells starting with =, +, - or @ are prefixed with a single quote so a spreadsheet does not treat them as formulas; use the JSON mapping when you need the exact original text.
  • JSON: one object per image, with all references nested in a references array:
[
  {
    "old_url": "https://old.example.com/img/a.png",
    "new_url": "https://img.example.com/i/R0RjUyP1UW",
    "image_id": "R0RjUyP1UW",
    "references": [
      { "source": "https://blog.example.com/post-1", "raw": "/img/a.png" }
    ]
  }
]

The mapping is for you to update your articles with; ImgYoung never modifies the source articles.

Deleting a job

A job that is Ready to confirm, Done, Cancelled or Aborted can be deleted; the job record, its items and its mapping are removed together, and images already imported are not affected. A Scanning or Importing job must be cancelled first. Download any mapping you need before deleting.

Limits and notes

  • Static HTML only: images that a script renders cannot be found, and such pages count under pages without images in the report.
  • Website mode produces a mapping: what is fetched is the rendered page, not the article's source file, so ImgYoung does not rewrite articles; you replace the addresses using the mapping.
  • Local file references are not recognized: data: inline images, local paths and Obsidian's ![[file]] are not fetchable addresses and are listed under unfetchable references. Use uploads for local images.
  • Formats: only JPEG, PNG, GIF and WebP can be imported; SVG, AVIF and others fail with upload_unsupported_type.
  • Public addresses only: as with fetching from a URL, private addresses are refused; see Fetch restrictions.
  • Scan limits: the scan report lists the first 1000 failed pages and the first 1000 unfetchable references, and counts the rest. 4 pages are fetched at a time, each sitemap, feed or page is capped at 50 MiB, and fetching one address takes at most 2 minutes. There is no limit on the number of pages or images. robots.txt is not read, and requests send User-Agent: ImgYoung/<version> (+https://github.com/x-haose/imgyoung).
  • Submission size: what you submit under Text and files is at most 32 MiB in total, each document at most 4 MiB, and parsing one document for more than 15 seconds is refused.

Images are fetched with a Referer: the one entered under Advanced options if there is one, otherwise the address of the page that references the image; with neither (for example an image URL list) none is sent. An old host that only allows its own pages answers such requests with a status such as 403, which shows as url_bad_status. Enter the old site's home page or an article address as the Referer and create the job again.

Calling it from a script

The import endpoints use signed-in session authentication and do not accept upload tokens. For authentication see the API reference; parameters and responses of each endpoint are in API reference: Admin · Bulk import.

After signing in, create a job from a list of image URLs (csrf_token comes from the login response):

Terminal
curl -b jar.txt \
  -H "X-CSRF-Token: <csrf_token>" \
  -H "Content-Type: application/json" \
  -d '{"mode": "images", "urls": ["https://old.example.com/img/a.png", "https://old.example.com/img/b.jpg"]}' \
  https://img.example.com/api/admin/imports

Download the mapping:

Terminal
curl -b jar.txt -OJ \
  "https://img.example.com/api/admin/imports/<job ID>/mapping?format=csv"

Error codes

ActionStatusError codeMeaning
Create400import_input_invalidThe request is invalid; the message says which part, for example an address that is not an absolute http or https address
Create413import_input_invalidThe request body exceeds 32 MiB
View, cancel, retry, delete, start, mapping404import_not_foundThe job does not exist or belongs to another user (administrators can see everyone's jobs)
Create429import_busyThe slots for parsing text are all taken and none became free within 15 seconds; try again later
Start, retry403import_owner_onlyAn administrator acted on someone else's job; only the creator can do these two
Cancel, retry, delete, start409import_state_invalidThe job's current state does not allow this action
Start400import_no_selectionNo pending image addresses under the selected domains
Retry409import_nothing_to_retryThere are no failed or unprocessed items

What each state allows:

StateShown asYou can
scanningScanningCancel
readyReady to confirmStart import, cancel, delete
importingImportingCancel
doneDoneRetry, download mapping, delete
cancelledCancelledRetry, download mapping, delete
abortedAbortedRetry, download mapping, delete

Error codes for failed items are in Failed items. The complete list is the ErrorCode schema in the API reference.

On this page