Use Case - Attachments

Upload files/blobs as attachments to Jungle.

You can attach files, blobs to any object in Jungle.

Some examples include shipping labels, login tags, and data exports.

Client workflow (Apps)

A recommended workflow for capturing a file from the user is to do the following:

  1. Show a file upload prompt, file selection, or take a photo.
  2. Once the file is selected, signature drawn, or photo taken, make the following query:
query UploadToken($path: ID!, $filename: String) {
  attachmentUploadToken(input: { object: { path: $path }, filename: $filename }) {
    __typename
    ... on UploadToken {
      url
      expiresAt
      bytesMax
    }
  }
}
  1. Using the url, in the client, complete a file upload to url.
  2. The attachment will be located at ${path}/attachments/${filename}.

If you need to do multiple file uploads, you'll need a signed URL per file.

View attachments

You may view attachments on an object:

query Attachments($path: ID!) {
  attachmentListV2(input: { object: { path: $path } }) {
    pageInfo {
      hasMore
      limit
    }
    results {
      attachment {
        path
        createdAt
        ... on Attachment {
          name
          tags
          contentType
          md5
          crc32c
          etag
          size
          modifiedAt
        }
      }
    }
  }
}

Download attachment

When a user wants to download a file, the client should request a URL for the attachment.

❗️Avoid this when listing attachments (.url has a high cost). Request the URL when the user actions a download.

query AttachmentDownloadUrl($path: ID!) {
  object(input: { id: $path, typename: "Attachment" }) {
    ... on Attachment {
      url
    }
  }
}

The provided download URL will be short-lived.

Location layouts

A location can have one layout: the image (PNG, JPEG or SVG, up to 20 MiB, or 20,971,520 bytes) its floor plan is traced over. When the image was made from a page of a PDF, the PDF can be kept with it as the layout's source, with the page and the part of the page the image shows. A PDF is never the layout itself: make an image of the page and send the PDF as the source. Each file is uploaded the same way as an attachment, then checked before it is kept.

SVG layouts must be well-formed UTF-8 XML with one SVG root in the SVG namespace. The following are refused with LOCATION_LAYOUT_UNSAFE_SVG:

  • Scripts, event-handler attributes, javascript: or vbscript: links, HTML elements and embedded documents such as foreignObject, iframe, object and embed.
  • Doctypes with internal declarations, including entities and default attributes. Only the standard SVG 1.0 and SVG 1.1 public doctypes are accepted.
  • Animation targeting a link (href, with any namespace prefix) or any event handler (on*), including targets written with character references.
  • References to another file in links, images, symbols or CSS url(). Empty references, fragments such as #aisle, and embedded PNG, JPEG, GIF or WebP data images are accepted. Embedded SVG data images are refused.
  • Stylesheet processing instructions. Backslashes, @import and image-set() are refused in style text and styling attributes, including presentation attributes and animation values targeting styles. Backslashes in descriptive values, such as accessible labels, are accepted.

Malformed XML is refused with LOCATION_LAYOUT_UNREADABLE. A drawing made only of shapes, internal gradients and internal symbols can be accepted. Each PDF source must contain page objects, and its claimed page counts must match those pages; page rendering is not checked.

  1. Request a place to upload to. null means you may not change the location. A layout made from a PDF needs two uploads, each with its own token: one for the image and one for the PDF.
query LocationLayoutUploadToken($locationId: ID!, $filename: String!) {
  locationLayoutUploadToken(input: { locationId: $locationId, filename: $filename }) {
    uploadId
    url
    expiresAt
    bytesMax
  }
}
  1. PUT each file's bytes to its url before expiresAt.
  2. Make it the location's layout. Leave out source when the image was not made from a PDF.
mutation LocationLayoutSet($locationId: ID!, $uploadId: ID!, $source: LocationLayoutSourceInput) {
  locationLayoutSet(input: { locationId: $locationId, uploadId: $uploadId, source: $source }) {
    ok
    ... on MutationFailure {
      code
      reason
    }
  }
}

With a source, the variables look like this. page is counted from 1. crop is the part of the page the image shows, in fractions (0 to 1) of the page as a viewer shows it, from its top left; x + width and y + height are at most 1. Leave out crop when the image shows the whole page.

{
  "locationId": "123",
  "uploadId": "<the image's uploadId>",
  "source": {
    "uploadId": "<the PDF's uploadId>",
    "page": 2,
    "crop": { "x": 0.1, "y": 0.2, "width": 0.5, "height": 0.6 }
  }
}

The kind of each file is decided from its content, not its name. When the layout is refused, both uploaded files are deleted, the location keeps the layout it had, and reason says why in words you can show to the user:

CodeMeaning
LOCATION_LAYOUT_UPLOAD_NOT_FOUNDNothing was uploaded for an uploadId, it expired, or it was already used.
LOCATION_LAYOUT_TOO_LARGEThe image or the PDF is larger than 20 MB.
LOCATION_LAYOUT_UNSUPPORTED_TYPEThe image is not a PNG, JPEG or SVG (a PDF included), or the source is not a PDF.
LOCATION_LAYOUT_UNSAFE_SVGThe SVG has scripts, event handlers or references to other files.
LOCATION_LAYOUT_UNREADABLEA file is damaged or incomplete, or the PDF has no pages.
LOCATION_LAYOUT_SOURCE_PAGE_NOT_FOUNDThe PDF has fewer pages than source.page.
INVALID_INPUTsource.page is less than 1, or source.crop is not a rectangle within the page.

An image or source uploadId can be accepted once, including after its layout is removed or replaced. Uploading again through the same address cannot change an accepted file. Setting a layout replaces the one the location had, with its source.

  1. Read it back:
query LocationLayout($locationId: ID!) {
  location(locationId: $locationId) {
    layout {
      kind
      width
      height
      createdBy
      createdAt
      file {
        name
        size
        url
      }
      source {
        file {
          name
          size
          url
        }
        pageCount
        page
        crop {
          x
          y
          width
          height
        }
      }
    }
  }
}

createdBy identifies who confirmed the layout: user|<user ID>, connection|<connection ID>, or unknown. Location layouts and their creator identifiers can currently be read across entities. createdAt gives the confirmation time. source is null when the image was not made from a PDF. The page and crop are as they were sent: the image is not checked against the PDF.

Each url is short-lived, and opening it directly downloads the file under the name it was uploaded with rather than showing it; load the image into an <img> and the PDF into a document viewer instead. locationLayoutRemove(input: { locationId }) removes the layout, its image and its source.

Can't find what you're looking for? Contact us