> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Network Storage

export const DynamicCodeBlock = ({language = 'yaml', filename, icon, lines, wrap, expandable, highlight, focus, children}) => {
  const STORAGE_KEY = 'upsun_versions_cache';
  const COMPOSABLE_STORAGE_KEY = 'upsun_composable_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/images';
  const COMPOSABLE_API_URL = 'https://meta.upsun.com/composable';
  const DEBUG_PREFIX = '[DynamicCodeBlock cache]';
  const [versionData, setVersionData] = useState(null);
  const [versionError, setVersionError] = useState(false);
  const [composableData, setComposableData] = useState(null);
  const [composableError, setComposableError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) {
              return cachedData;
            }
          }
        } catch (err) {
          console.error('Failed to load from cache:', err);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      console.debug(`${DEBUG_PREFIX} revalidating`, {
        storageKey: STORAGE_KEY,
        hasCachedData: Boolean(cachedData),
        hasCachedEtag: Boolean(cachedEtag)
      });
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        console.debug(`${DEBUG_PREFIX} revalidated (304)`, {
          storageKey: STORAGE_KEY
        });
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (err) {
            console.error('Failed to refresh cache metadata:', err);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      console.debug(`${DEBUG_PREFIX} refreshed (200)`, {
        storageKey: STORAGE_KEY,
        etag
      });
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (err) {
          console.error('Failed to cache data:', err);
        }
      }
      return data;
    };
    fetchData().then(data => setVersionData(data)).catch(err => console.error('Failed to fetch version data:', err));
  }, []);
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  const getVersion = (lang, requestedVersion = 'latest') => {
    if (lang === 'composable') {
      if (!composableData || !composableData.versions || Object.keys(composableData.versions).length === 0) return null;
      if (requestedVersion && requestedVersion !== 'latest') {
        return (requestedVersion in composableData.versions) ? requestedVersion : null;
      }
      return findHighestVersion(composableData.versions);
    }
    if (!versionData) return null;
    const imageData = versionData[lang];
    if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
      return null;
    }
    if (requestedVersion && requestedVersion !== 'latest') {
      return (requestedVersion in imageData.versions) ? requestedVersion : null;
    }
    return findHighestVersion(imageData.versions);
  };
  let code = typeof children === 'string' ? children : String(children || '');
  const codeLines = code.split('\n');
  while (codeLines.length > 0 && codeLines[0].trim() === '') codeLines.shift();
  while (codeLines.length > 0 && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
  if (codeLines.length > 0) {
    const indents = codeLines.filter(line => line.trim().length > 0).map(line => line.match(/^[ \t]*/)[0].length);
    const minIndent = Math.min(...indents);
    code = codeLines.map(line => line.slice(minIndent)).join('\n');
  }
  code = code.replace(/\{\{version:(.*?)\}\}/g, (match, params) => {
    const parts = params.split(':');
    const lang = parts[0];
    const ver = parts[1] || 'latest';
    const isComposable = lang === 'composable';
    const hasError = isComposable ? composableError : versionError;
    const dataReady = isComposable ? composableData !== null : versionData !== null;
    if (hasError) return '(unavailable)';
    if (dataReady) {
      const resolvedVersion = getVersion(lang, ver);
      return resolvedVersion || match;
    }
    return '...';
  });
  const codeBlockProps = {
    language,
    ...filename && ({
      filename
    }),
    ...icon && ({
      icon
    }),
    ...lines !== undefined && ({
      lines
    }),
    ...wrap !== undefined && ({
      wrap
    }),
    ...expandable !== undefined && ({
      expandable
    }),
    ...highlight && ({
      highlight
    }),
    ...focus && ({
      focus
    })
  };
  return <CodeBlock {...codeBlockProps}>{code}</CodeBlock>;
};

export const VersionDeprecatedBlock = () => <>
    <h3 id="deprecated-versions">Deprecated versions</h3>
    <p>
    The following versions are <a href="/docs/glossary#deprecated-versions">deprecated</a>.
    They're available, but they don't receive security updates from upstream and aren't guaranteed to work.
    They'll be removed in the future – consider migrating to a <a href="#supported-versions">supported version</a>.
    </p>
  </>;

export const MetaImageVersionList = ({language, status}) => {
  const [versions, setVersions] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const STORAGE_KEY = 'upsun_versions_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/images';
  useEffect(() => {
    if (!language) {
      setLoading(false);
      return;
    }
    setLoading(true);
    setError(null);
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) return cachedData;
          }
        } catch (error_) {
          console.error('Failed to load from cache:', error_);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (error_) {
            console.error('Failed to refresh cache metadata:', error_);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (error_) {
          console.error('Failed to cache data:', error_);
        }
      }
      return data;
    };
    fetchData().then(data => {
      if (!data || !data[language]) {
        setVersions([]);
        setLoading(false);
        return;
      }
      const imageData = data[language];
      if (!imageData.versions) {
        setVersions([]);
        setLoading(false);
        return;
      }
      let versionList = Object.entries(imageData.versions).map(([name, v]) => ({
        name,
        status: v.upsun?.status || v.status
      })).sort((a, b) => {
        const aParts = a.name.split('.').map(Number);
        const bParts = b.name.split('.').map(Number);
        const max = Math.max(aParts.length, bParts.length);
        for (let i = 0; i < max; i++) {
          const av = aParts[i] || 0;
          const bv = bParts[i] || 0;
          if (av !== bv) return bv - av;
        }
        return 0;
      });
      if (status) {
        versionList = versionList.filter(v => v.status === status);
      }
      setVersions(versionList);
      setLoading(false);
    }).catch(error_ => {
      console.error('MetaImageVersionList error:', error_);
      setError(error_.message);
      setLoading(false);
    });
  }, [language, status]);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!versions || versions.length === 0) {
    if (status === 'incoming') return null;
    return <p>No versions available! Contact support.</p>;
  }
  let incomingBlock = null;
  if (status === 'incoming' && versions.length > 0) {
    incomingBlock = `These versions are not yet available but are expected to be released soon.`;
  }
  return incomingBlock ? <Note>
      <p>{incomingBlock}</p>
      <ul>
        {versions.map(version => <li className="image-version" key={version.name}>
            {version.name} {version.status === 'beta' && <span className="badge">Beta</span>}
          </li>)}
      </ul>
    </Note> : <ul>
      {versions.map(version => <li className="image-version" key={version.name}>
          {version.name} {version.status === 'beta' && <span className="badge">Beta</span>}
        </li>)}
    </ul>;
};

export const MetaImageVersion = ({language, version}) => {
  const [selectedVersion, setSelectedVersion] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const isComposable = language === 'composable';
  const STORAGE_KEY = isComposable ? 'upsun_composable_cache' : 'upsun_versions_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = isComposable ? 'https://meta.upsun.com/composable' : 'https://meta.upsun.com/images';
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  useEffect(() => {
    if (!language) {
      setLoading(false);
      return;
    }
    setLoading(true);
    setError(null);
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) return cachedData;
          }
        } catch (error_) {
          console.error('Failed to load from cache:', error_);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (error_) {
            console.error('Failed to refresh cache metadata:', error_);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (error_) {
          console.error('Failed to cache data:', error_);
        }
      }
      return data;
    };
    fetchData().then(data => {
      if (!data) {
        setSelectedVersion(null);
        setLoading(false);
        return;
      }
      const imageData = isComposable ? data : data[language];
      if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
        setSelectedVersion(null);
        setLoading(false);
        return;
      }
      let versionName = null;
      if (version && version !== 'latest') {
        versionName = (version in imageData.versions) ? version : null;
      } else {
        versionName = findHighestVersion(imageData.versions);
      }
      setSelectedVersion(versionName);
      setLoading(false);
    }).catch(error_ => {
      console.error('MetaImageVersion error:', error_);
      setError(error_.message);
      setLoading(false);
    });
  }, [language, version]);
  if (loading) return <span>…</span>;
  if (error) return <span title={error}>⚠ unavailable</span>;
  if (!selectedVersion) return <span>No version found</span>;
  return <span>{selectedVersion}</span>;
};

The Network Storage service enables a new kind of [mount](/docs/configure-apps/image-properties/mounts)
that refers to a shared service rather than to a local directory.
This service allows you to store data and share it between different apps.

## Supported versions

You can select the major and minor version.

Patch versions are applied periodically for bug fixes and the like.
When you deploy your app, you always get the latest available patches.

<MetaImageVersionList language="network-storage" status="supported" platform="grid" />

<MetaImageVersionList language="network-storage" status="incoming" platform="grid" />

This service is the Upsun network storage implementation, not the version of a third-party application.

<Warning>
  It isn't possible to upgrade or downgrade the network storage service version while keeping existing data in place.
  Changing the service version requires that the service be reinitialized.
  Any change to the service version results in existing data becoming inaccessible.
</Warning>

<VersionDeprecatedBlock />

<MetaImageVersionList language="network-storage" status="deprecated" platform="grid" />

## Retired versions

The following versions have been retired and are no longer available.
If your project uses a retired version, you must update to a [supported version](#supported-versions).

<MetaImageVersionList language="network-storage" status="retired" platform="grid" />

## Usage example

### 1. Configure the service

To define the service, use the `network-storage` type:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      services:
        # The name of the service container. Must be unique within a project.
        <SERVICE_NAME>:
          type: network-storage:<VERSION>
    `
  }
</DynamicCodeBlock>

`SERVICE_NAME` must be [RFC 1123](https://tools.ietf.org/html/rfc1123) compliant, and as such it must:

* Contain at most 63 characters
* Contain only lowercase alphanumeric characters or `-` (underscores `_` are not allowed)
* Start with an alphanumeric character
* End with an alphanumeric character

This is due to the fact that `SERVICE_NAME` is used as hostname for the network storage.

Note that changing the name of the service replaces it with a brand new service and all existing data is lost.
Back up your data before changing the service.

### 2. Add the mount

To define the mount accessible by your application, use the following configuration:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        # The name of the app container. Must be unique within a project.
        <APP_NAME>:
          mounts:
            <TARGET_PATH>:
              source: service
              service: <SERVICE_NAME>
              source_path: <SOURCE_PATH>

      services:
        # The name of the service container. Must be unique within a project.
        <SERVICE_NAME>:
          type: network-storage:<VERSION>
    `
  }
</DynamicCodeBlock>

* `<TARGET_PATH>` is the path to your mount within the app container (relative to the app’s root).
* `SERVICE_NAME` is the name you [defined in step 1](#1-configure-the-service).
* `<SOURCE_PATH>` specifies where the mount points inside the service.<br />
  If the `source_path` is an empty string (`""`), your mount points to the entire service.<br />
  If you don’t define a `source_path`, Upsun uses the `MOUNT_PATH` as default value, without leading or trailing slashes.
  For example, if your mount lives in the `/my/files/` directory within your app container, it will point to a `my/files` directory within the service.

### Example configuration

<DynamicCodeBlock language="yaml">
  {`
      applications:
        # The name of the app container. Must be unique within a project.
        myapp:
          mounts:
            'my/files':
              source: service
              service: network-storage
              source_path: files

      services:
        # The name of the service container. Must be unique within a project.
        network-storage:
          type: network-storage:{{version:network-storage:latest}}`
  }
</DynamicCodeBlock>

## Multi-application usage

If your project contains [multiple apps](/docs/configure-apps/multi-app),
they may [share `storage` mounts](/docs/configure-apps/image-properties/mounts#share-a-mount-between-several-apps).

Alternatively, they may use shared `service` mounts.
If the `source_path` is the same for both apps,
the files are shared between the two applications even if the mount location is different.

It's also possible to have one app mount a `source_path` that's a subdirectory of another application's mount.
For example:

```yaml .upsun/config.yaml theme={null}
applications:
  # The name of the app container. Must be unique within a project.
  app1:
    # The location of the application's code.
    source:
      root: "app1"

    [...]

    mounts:
      # The path to your mount within the app container (relative to the app's root).
      'web/uploads':
        # Specifies that the mount points to a network storage service that can be shared between apps.
        source: service
        # The name of the network storage service the mount points to.
        service: network-storage
        # Specifies where your mount points inside the external directory that is mounted to your app container.
        source_path: uploads

  # The name of the app container. Must be unique within a project.
  app2:
    # The location of the application's code.
    source:
      root: "app2"

    [...]

    mounts:
      # The path to your mount within the app container (relative to the app's root).
      'process':
        # Specifies that the mount points to a network storage service that can be shared between apps.
        source: service
        # The name of the network storage service the mount points to.
        service: network-storage
        # Specifies where your mount points inside the external directory that is mounted to your app container.
        # Since the target is the uploads directory app1's mount already points to,
        # the network storage service is effectively shared between app1 and app2.
        source_path: uploads/incoming
      # The path to your mount within the app container (relative to the app's root).
      'done':
        # Specifies that the mount points to a network storage service that can be shared between apps.
        source: service
        # The name of the network storage service the mount points to.
        service: network-storage
        # Specifies where your mount points inside the external directory that is mounted to your app container.
        # Since the target is the uploads directory app1's mount already points to,
        # the network storage service is effectively shared between app1 and app2.
        source_path: uploads/done
```

In this example, `app1` has access to the entire `uploads` directory by writing to `web/uploads`.
`app2` has two mounts that it can write to: `process` and `done`.
The `process` mount refers to the same directory as the `web/uploads/incoming` directory does on `app1`,
and the `done` mount refers to the same directory as the `web/uploads/done` directory on `app1`.

## How do I give my workers access to my main application’s files?

If you need to use a worker with access to the same file mount as your web-serving app,
define all the necessary mounts as `service` mounts.

The following example assumes a Network Storage service named `files` has been defined in `.upsun/config.yaml`.
Drupal files directories are shared between the `web` and `worker` instances,
while the Drush backup directory is unique to the `web` instance.

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          source:
            root: "/"

          type: "php:{{version:php:latest}}"

          [...]

          mounts:
            # The public and private files directories are
            # network mounts shared by web and workers.
            'web/sites/default/files':
              source: service
              service: files
              source_path: files
            'private':
              source: service
              service: files
              source_path: private
            # The backup, temp, and cache directories for
            # Drupal's CLI tools don't need to be shared between web and workers.
            # It wouldn't hurt anything to make them network
            # shares, however.
            '/.drush':
              source: storage
              source_path: drush
            'tmp':
              source: tmp
              source_path: tmp
            'drush-backups':
              source: storage
              source_path: drush-backups
            '/.console':
              source: storage
              source_path: console

          # Crons run on the web container, so they have the
          # same mounts as the web container.
          crons:
            drupal:
              spec: '*/20 * * * *'
              commands:
                start: 'cd web ; drush core-cron'

          # The worker defined here also has the same 6 mounts;
          # 2 of them are shared with the web container,
          # the other 4 are local to the worker.
          workers:
            queue:
              commands:
                start: |
                  cd web && drush queue-run myqueue`
  }
</DynamicCodeBlock>

## How can I migrate data from a `storage` mount to a `service` mount?

Network Storage `service` mounts can be shared between different apps,
while `storage` mounts can only be shared between different *instances* of the same app.
To move data from a `storage` mount to a `service` one, follow these instructions.

Assuming you have the following `storage` mount:

```yaml .upsun/config.yaml theme={null}
applications:
  myapp:

    [...]

    mounts:
      web/uploads:
        source: storage
        source_path: uploads
```

1. Add a new `network-storage` service to your configuration:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:

          [...]

          mounts:
            web/uploads:
              source: storage
              source_path: uploads

      services:
        # The name of the service container. Must be unique within a project.
        network-storage:
          type: network-storage:{{version:network-storage:latest}}`
  }
</DynamicCodeBlock>

<Note>
  Make sure you [allocate enough disk space](/docs/manage-resources/adjust-resources#vertical-scaling) to your `network-storage` service
  for your existing files with some buffer.
</Note>

2. Add a new `service` mount, named `new-uploads`:

<DynamicCodeBlock language="yaml">
  {`
      applications:
        myapp:

          [...]

          mounts:
            web/uploads:
              source: storage
              source_path: uploads
            new-uploads:
              source: service
              service: network-storage
              source_path: uploads

      services:
        # The name of the service container. Must be unique within a project.
        network-storage:
          type: network-storage:{{version:network-storage:latest}}`
  }
</DynamicCodeBlock>

Note that each mount is on a different storage service, which is why they can have the same `source_path`.

3. Deploy your changes.

4. Copy all your files from the `storage` (`web/uploads`) mount to the `service` (`new-uploads`) mount using `rsync`:

   ```bash theme={null}
   rsync -avz web/uploads/* new-uploads/
   ```

5. Reverse the mounts.
   To do so, rename the `storage` mount to `old-uploads`, and point the `web/uploads` directory to the `service` mount:

<DynamicCodeBlock language="yaml">
  {`
      applications:
        myapp:

          [...]

          mounts:
            old-uploads:
              source: storage
              source_path: uploads
            web/uploads:
              source: service
              service: network-storage
              source_path: uploads

      services:
        # The name of the service container. Must be unique within a project.
        network-storage:
          type: network-storage:{{version:network-storage:latest}}`
  }
</DynamicCodeBlock>

6. Push your changes and check that the files are now accessible from the `service` mount (now named `web/uploads`).
   To check that no files were lost during the transfer, run the following command:

   ```bash theme={null}
   rsync -avz old-uploads/* web/uploads/
   ```

7. Delete the contents of the `old-uploads` `storage` mount before removing it.

8. Push your changes again.
