AeroFS for Developers

Unleash the power of distributed storage

AeroFS Private Cloud provides a RESTful API for content access and OAuth 2.0 for user authorization. Once an appliance administrator registers your app with the organization's AeroFS Appliance and a user in the organization gives authorization, your app can access the user's AeroFS data by making requests to the API endpoint in the AeroFS Appliance.

How it works

  • Your app makes API requests to the API endpoint in the AeroFS Appliance.
  • The load balancer in the AeroFS Appliance forwards API requests to Team Servers and Desktop clients.
  • A computer is chosen based on your app's consistency policy.

Unlike traditional systems where all the data are stored on a central server, AeroFS replicates files peer-to-peer across all the Team Servers and desktop clients in an organization.

A load balancer running in the AeroFS Appliance acts as a single API endpoint, and securely forwards API requests to running Team Servers and desktop clients. It distributes workload and maintains a balance between service availablity and data consistency.

Next steps

Tutorial

Read this tutorial and write your first AeroFS app! The full API reference is available here.

API mailing list

Join our mailing list to be notified of API updates.

Feedback

Any comments about the AeroFS API? Send your ideas to api@aerofs.com.

Create your first AeroFS app

Welcome! In this article we will introduce everything you need to know to build a very basic AeroFS app. For complete API reference check out this document.

Step 1: Set up a Private Cloud instance

The AeroFS API is available in the AeroFS Private Cloud only. You need to set up a Private Cloud environment, which will be used as a sandbox for testing purposes.

A. Request a developer license
Request a license which is free for development purposes.
B. Set up the appliance
Once a license is issued, download and install the AeroFS Appliance. Check out this article for system requirements for Appliance setup. In the following text, we use host.name to represent the hostname of this appliance.
C. Set up a desktop client

A running desktop client or Team Server is needed for your app to access user content, and it is easier to inspect data using a client.

After creating the first user account in the above step, download the desktop client from the appliance and set it up using the user account. Do not install on a computer with production AeroFS installed, as it would overwrite the production installation.

If you want to start over, simply set up a new Private Cloud instance. Just remember to unlink and uninstall desktop clients and Team Servers cleanly before setting up a new instance, as these applications will be incompatible with new instances.

Step 2: Register an app

Go to http://host.name:8484/register_app to register your first app into the appliance. Set the name of the app "My First App" and the redirect URL http://blackhole. In the real world, the URL should be the address of your app. For testing, we use a non-existing URL to avoid the hassle of setting up a Web server.

Once the registration is done, the Web page shows you the Client ID and Client Secret of the new app, which are needed for Step 3. An example is shown in the screenshot to the right. You can revisit this page at http://host.name:8484/registered_apps at any time.

Step 3: Get an access token

This and later steps will become part of your app, written in a programming language that you prefer. In this tutorial, however, we use the command-line tool cURL to demonstrate raw message exchanges.

A. Ask the user to authorize your app

Your app should automatically send the user to the following URL so that the user can authorize the app to their AeroFS account:

https://host.name/authorize?response_type=code&redirect_uri=http://blackhole&client_id=30fd5241-02ea-46d5-ab07-f0aeba4164c3

For testing, replace the client_id argument in the above URL with the Client ID you obtained in Step 2, and visit this URL in your browser. Note that the redirect_uri argument must be identical to the redirect URL you entered in Step 2.

B. Obtain an access token

Once the user presses the Accept button on the above page, they will be directed to your app's redirect URL. An authorization code is included in the URL. Your app should use this code to exchange for an access token.

For testing, click the Accept button. Since we didn't provide a valid redirect URL, the browser will display this URL in the address bar and complain that the address is not found:

http://blackhole?code=67926e86a873492582cf68874f71a112

Then, use the code in the URL to request for an access token using this command:

$ curl -i -X POST https://host.name/auth/token \
    -d 'client_id=30fd5241-02ea-46d5-ab07-f0aeba4164c3' \
    -d 'client_secret=0212ff9a-febe-4eb2-96f0-424adb5aa47d' \
    -d 'redirect_uri=http://blackhole' \
    -d 'grant_type=authorization_code' \
    -d 'code=67926e86a873492582cf68874f71a112'

Remember to replace the code argument with the code in the URL displayed by your browser, and client_id and client_secret with the values you obtained in Step 2.

If the request is successful, you should expect the following JSON object in the response body:

{
   "access_token": "b383965fbb284c4fa95bebb3a5f392e8",
   "token_type": "bearer",
   "expires_in": 0,
   "scope": ""
}

As a security measure, the authorization code is valid only for a few seconds. If the request is not made in time, you may get "400 Bad Request" with an error description "the authorization code is invalid". In this case, simply run Step 3A again to obtain a new code.

The access_token you get from the successful response will be used for all subsequent API requests. The zero-valued expires_in field indicates that the token never expires until the user revokes the app from their accounts.

Step 4: List folders

To list children under the root AeroFS folder, call GET /children with no arguments:

$ curl -i https://host.name/api/v0.10/children \
    -H 'Authorization: Bearer b383965fbb284c4fa95bebb3a5f392e8'

The "/api/v0.10" string is a common prefix for all REST API calls. The "Authorization" header carries the access token. Remember to replace the string "b383..." with your own token obtained in Step 3B.

Important: if your appliance uses a self-signed SSL certificate, cURL will block your requests by default. For testing, you can use the cURL's --insecure flag. However, any app you write should be configured to trust the appliance's SSL certificate. Click here for more about app configuration.

The response body of a successful response is a Children object:

{
   "parent": "3293523a4ca93cd5bcc17d4c4a00946800000000000000000000000000000000",
   "folders": [],
   "files": []
}

The parent field is the identifier of the root folder. The folders and files fields are empty because we haven't created any files or folders. Otherwise, they should contain a list of Folder or File objects.

If no clients or Team Servers are running at the time the request is made, you would expect a "503 Service Unavailable" response.

Step 5: Upload files

A. Create a new file

An empty file must be created with POST /files before populating file content:

$ curl -i -X POST https://host.name/api/v0.10/files \
    -H 'Content-Type: application/json' \
    -d '{"parent": "3293523a4ca93cd5bcc17d4c4a00946800000000000000000000000000000000", "name": "hello.txt"}' \
    -H 'Authorization: Bearer b383965fbb284c4fa95bebb3a5f392e8'

The request body is specified in the -d option. Replace its parent field with the root folder ID you obtained in Step 4, to create a file "hello.txt" under your root AeroFS folder. Also replace the access token as before.

The response body of a success response is a File object:

{
   "id": "3293523a4ca93cd5bcc17d4c4a009468d1e675b41e8d4c309fc9796a899ee405",
   "name": "hello.txt"
   "last_modified": "2014-01-01T00:32:58Z",
   "size": 0,
   "mime_type": "text/plain",
}

The id field is the identifier of the newly created file.

B. Populate file content

First, create a file locally:

$ echo 'Hello, world!' > hello.upload.txt

Then, upload this file by calling PUT /files/{id}/content, with {id} being the identifier of the target file:

$ curl -i -X PUT https://host.name/api/v0.10/files/3293523a4ca93cd5bcc17d4c4a009468d1e675b41e8d4c309fc9796a899ee405/content \
    -T hello.upload.txt \
    -H 'Authorization: Bearer 845a1a4512164fc786da886f1320173a'

Replace "3293..." with the file ID obtained from Step 5A. Also replace the access token as before.

The server will return 200 with an empty body once the upload is successful.

Step 6: Download files

Download a file's content by calling GET /files/{id}/content with {id} being the file's identifier:

$ curl -i https://host.name/api/v0.10/files/3293523a4ca93cd5bcc17d4c4a009468d1e675b41e8d4c309fc9796a899ee405/content

Use the identifier obtained in Step 5A in the URL. Alternatively, call GET /children as demonstrated by Step 4, and retrieve file identifiers from the response.

There are options such as range requests and conditional requests for file downloading. Check out the API doc for advanced usage.

Next steps

We've covered the basic stuff for your AeroFS app. Skim through the API reference to see what else your app can do with the API. Email api@aerofs.com for issues and other comments, or if you are interested in helping us build SDKs. Once your app is ready to go, package and ship it!

Choose the right consistency policy

When an API request is made, the load balancer selects a Team Server to serve the request. If your organization does not use a Team Server, or if there is no Team Server available, a Desktop Client will be selected to serve the request.

If there are multiple computers that can serve a request, the load balancer's selection algorithm impacts the service availability and data consistency observed by the end user. For example, if a computer becomes unavailable after being used by the load balancer to serve a request, the load balancer may decide to use a different computer to serve the user's next request. If the two computers are not fully in sync, the two requests may return inconsistent results.

Your app can dictate how the load balancer selects computers. To deliver a satisfying user experience, it is important to understand your users' needs and pick the right policy for your app.

The default policy

By default, the load balancer uses the same computer for all the requests within an HTTP session between the app and the load balancer. A new computer is selected only if the originally selected computer becomes unreachable. This policy is suitable for most apps.

To apply the default policy, simply make sure HTTP cookies are enabled in your app.

Prefer availability over consistency

Under this policy, the load balancer may freely select different computers to serve different requests. This gives the system flexibility to choose the most available computers based on performance and load distribution, at the cost of potential inconsistency the end user may observe.

To use this policy, omit cookies when making requests. This effectively ends the HTTP session after each request.

Prefer consistency over availability

This policy doesn't allow the load balancer to change the selected computer in an HTTP session. An error will be returned if the selected computer becomes unavailable. This is the correct policy for an app which requires highly consistent behavior.

Enable cookies and specify Endpoint-Consistency: strict in the request headers to use this policy.

Note: this policy allows your app to control how consistency is maintained within an HTTP session, but not across sessions. Furthermore, your app cannot specify how the load balancer selects the first computer for a session. Thus, this policy implements session consistency. Learn more about consistency models in a distributed system.

Publish your app

Package it

Unlike traditional API models where all apps are registered with a single web service, AeroFS apps must be registered with individual AeroFS Private Cloud instances by the appliance administrators. Your app will therefore require some configuration for data that is different between AeroFS Private Cloud instances. Your app should accept four configuration values:

Client ID:
The unique identifier your app presents to the OAuth server
Client secret:
The password your app uses to authenticate with the OAuth server
Hostname:
The hostname the AeroFS Appliance
Browser certificate:
The SSL certificate of the AeroFS Appliance in x509 format. Your app should always use this certificate to verify the authenticity of the API endpoint.

When an appliance administrator registers an app, the AeroFS Appliance supplies the admin with a JSON data file that includes all the fields described above. To deliver a consistent user experience, all AeroFS apps should prompt the admin to upload this file when the admin configures the app.

Below is an example of the file content:

{
    "hostname":  "host.name",
    "cert":  "-----BEGIN CERTIFICATE-----\nMIIFKjC...",
    "client_id":  "30fd5241-02ea-46d5-ab07-f0aeba4164c3",
    "client_secret":  "0212ff9a-febe-4eb2-96f0-424adb5aa47d"
}

Publish it

As per our license agreement, you may use your app immediately for any internal usage.

If you'd like to publish your app for public distribution, drop us a line at api@aerofs.com with a brief description as well as a demo URL of your app. We will work with you to publish your app to our customers and partners.

Respond to AeroFS events

Introduction

Purpose

System administrators should be able to determine usage and sharing patterns for their organization. This includes near-real-time status information as well as historical data.

The purpose of the audit feature is to:

  • Enable insights into how data is shared and accessed.
  • Provide near-real-time monitoring of the flow of data through the organization, so the administrator can detect and prevent data leakage.
  • Help retroactively identify the root causes of data leakage.

We provide these features by publishing auditable events to existing IT infrastructure for event management.

AeroFS Audit Service

The AeroFS Audit Service enables all the other features described in this reference. It is the central point that accepts auditable events from various sources, applies formatting as needed, and delivers the events to one or more downstream systems.

Auditable Events from Clients

We call an AeroFS client "offline" if it is unable to reach the AeroFS Appliance; for instance, in a segregated external network. An offline client may still perform local-network file transfers with certified peers.

If client file-transfer auditing is enabled, AeroFS clients will upload the file transfer events to an audit collection service on the AeroFS Appliance. Clients cannot locally opt-out. Activity logs of local-network file transfer events will be batch-uploaded to the AeroFS Appliance when the client is next online.

Delivery to Downstream Systems

Auditable events must be delivered to a downstream event capture and analysis system. The site administrator will choose the destination system or systems.

The channel encapsulates configuration of the transport, network address, and message protocol to deliver.

The message protocols supported today are JSON documents sent over a TCP or TCP-SSL stream. In the case of the TCP-SSL stream, the administrator can supply an explicit certificate in PEM format for the downstream system. This channel can be used to connect to a Splunk server on a network listener port. More information on configuring downstream systems can be found in our help center documentation.

Event Contents

All audit events include the following:

topic
The coarse topic the event belongs to. Must be one of { FILE, USER, SHARING, DEVICE, LINK, ORG }.
event
The name of the event.
timestamp
Time the event was published. This may differ from the time received by the downstream system.

Additional parameters are documented with the events, grouped by topic, below.

Event Names

The following sections describe the set of events that can be published by the AeroFS Appliance services. Some events may not apply in certain deployment configurations.

File Transfer

File Transfer Events

Events related to files, specifically files transferred between AeroFS clients.

file.notification (verified_submitter, soid, event_time, operations, device, path, [path_to], [mobile_device])
A local file notification.
file.transfer (verified_submitter, soid, event_time, operations, device, path, [path_to], destination_device, destination_user)
A remote file transfer event. The reporting device is the source of the event.
File Transfer Parameters
  • verified_submitter: The reporting user and the certified device that submitted the event.
  • soid: Uniquely identify the store and object of a file or folder.
  • event_time: Time the actual file event took place, as reported by the client.
  • operations: An array of file operations. The actual file operation is the union of these operations; for instance, a single operation could include MODIFY and CREATE operations. The set of operations are:
    • CREATE (file is created)
    • MODIFY (file is modified)
    • MOVE (file is renamed or moved)
    • DELETE (file is deleted)
    • META_REQUEST (request for version/meta info)
    • CONTENT_REQUEST (request to start copying file content)
    • CONTENT_COMPLETED (file transfer completed)
  • device: The device reporting the event.
  • destination_device: The destination device, if applicable.
  • destination_user: The user who owns the destination device, if applicable.
  • path, path_to: A Path object.
  • User Management

    Events related to the creation, management, and roles of users.

    User Management Events
    user.signin (user, authority)
    User signed in to AeroFS via the web or desktop client.
    device.mobile.error (user)
    User tried to certify a mobile device but encountered a permission or credential error.
    user.password.change (user)
    User changed their AeroFS password. Note that this is only supported for users with local credentials - OpenID and LDAP-authenticated users cannot use this function.
    user.org.invite (inviter, invitee, organization)
    User sent an invite to the given organization to an email that does not currently have an AeroFS account.
    user.org.signup (user, first_name, last_name, organization, is_admin)
    User signed up after receiving an invite code.
    user.create (email, caller)
    Admin created a user account via the User Management API.
    user.delete (email, delete)
    Admin deleted a user account via the User Management API.
    user.password.error (user)
    A user signin action was denied due to a bad credential.
    user.password.reset.request (user, caller)
    A user requested a password reset token to their email. This cannot occur for users that are associated with an OpenID or LDAP authority.
    user.password.reset (user)
    A user reset their own password using the email password reset token. This cannot occur for users that are associated with an OpenID or LDAP authority.
    user.password.revoke (email, caller)
    Admin revoked a user credential via the User Management API.
    user.org.authorization (admin_user, target_user, new_level)
    User changed organizational permission level; either promoted to an admin or demoted to a regular user.
    user.password.update (email, caller)
    User or administrator updated a user credential via the User Management API.
    user.update (email, caller)
    User or administrator updated a user name via the User Management API.
    user.org.provision (user, authority)
    Auto-provisioned user signed in to AeroFS for the first time. authority is the type of the identity-authenticating authority.
    user.org.accept (user, previous_org, new_org)
    User accepted an invitation to an organization.
    user.org.remove (admin_user, target_user, organization)
    The admin user has remove the target user from the given organization.
    user.account.request (email)
    A user requested an AeroFS account. NOTE: the AeroFS appliance may be configured to disallow open signup (invite-only); in that case this event will not occur.
    user.quota.warning (user, bytes_used, bytes_allowed)
    A user has reached 80% of their disk usage quota as reported by a teamserver.
    user.2fa.enable (user)
    User enabled two-factor authentication on their account.
    user.2fa.disable (user, caller)
    User or administrator disabled two-factor authentication on their account.
    User Management Parameters
    • authority: Authority type associated with a user signin action. The authority may be credential (local password), LDAP, or OpenID.
    • caller: The verified caller information for the user that initiated and authorized the user action.
    • user, admin_user, target_user: UserID (generally an email address) of an existing AeroFS user. User ID's starting with a colon are special pseudo-users associated with Team Servers.
    • email: Email address of a person in the invitation process but without a current AeroFS account.
    • first_name, last_name: User name details.
    • previous_org, new_org, organization: Organization ID, as used in different contexts.
    • inviter: UserID of the person sending an AeroFS invite.
    • invitee: Email address of a person receiving an AeroFS invite.
    • new_level: An organizational auth level - either USER or ADMIN.
    • bytes_used: The size of the user's AeroFS folder in bytes, as measured by a TeamServer
    • bytes_allowed: The allowed size of the user's AeroFS folder in bytes, as set by the org admin

    Sharing

    Events related to folder sharing in AeroFS.

    Sharing Events
    folder.create (folder, caller)
    User creates a shared folder.
    folder.destroy (folder, caller)
    User destroys a shared folder.
    folder.join (folder, target, caller, role, [join_as])
    User accepts a share invitation.
    folder.leave (folder, caller, target)
    User leaves shared folder.
    folder.invite (folder, sharer, target, role)
    User sends a share invitation to an internal or external user.
    folder.delete_invitation (folder, caller, target)
    User deletes an invitation to a shared folder.
    folder.permission.delete (folder, caller, target)
    User is removed from a shared folder permission list.
    folder.permission.update (admin_user, target, old_role, new_role, folder)
    Shared-folder owner changes a User's role for a folder.
    Sharing Parameters
    • folder: A Shared Folder object.
    • target: UserID of a person invited to a shared folder.
    • caller: A Caller object that indicates the person sending an invite to a shared folder or making a change to shared folder. Note this may include a mobile Device ID if the request is made via the Content API.
    • role, new_role, old_role: A Role object for a Shared Folder invitation.

    Device Management

    Events related to device certification and decomissioning.
    Device Management Events
    device.signin (user, device_id, ip)
    A device successfully signs into the AeroFS appliance using a trusted certificate signed by the AeroFS certificate authority.
    device.certify (user, device_type, device_id, [device_name], [os_family], [os_name])
    A new device successfully certified with AeroFS. Some fields may not be populated for mobile devices.
    device.mobile.code (user, timeout)
    User generated a one-time access code for authenticating a mobile device.
    device.mobile.authenticate (user, device)
    User authenticates a mobile device using a one-time access code.
    device.recertify (user, device, device_type)
    Device replaced an existing certificate with a newer one.
    device.unlink (admin_user, device, owner)
    Device (owned by owner) is unlinked per request from user admin_user. Unlink means the device will keep the files it has, but it will no longer send or receive file updates.
    device.erase (admin_user, device, owner)
    Device (owned by owner) is remote-erased per request from user admin_user. The contents of the AeroFS folder on the specified device will be wiped.
    Device Management Parameters
    • user, owner: UserID that certified the reporting device.
    • admin_user: UserID of the user that requested a device action. May be the device owner or an organization administrator.
    • device, device_id: Device ID of the reporting device.
    • device_name: User-visible name of the device (typically the computer or mobile device name)
    • device_type: The type of AeroFS device reporting. This will be one of Desktop Client, Team Server, or Mobile App.
    • os_family: Textual OS family name (Windows, MacOS, Linux)
    • os_name: Specific name of the reporting OS.
    • timeout: Timeout in seconds of a mobile access code.

    Link-Based Sharing Events

    Events related to link-based sharing of files and folders.

    Link-Based Sharing Events
    link.create (ip, caller, key, soid)
    User created a link to a file or folder.
    link.delete (ip, caller, key)
    User deleted a link to a file or folder.
    link.access (ip, key)
    User used the link to access the metadata of the object.
    link.download (ip, key, oid, name)
    User used the link to download an object.
    link.set_password (ip, caller, key)
    User set a password for the link.
    link.remove_password (ip, caller, key)
    User removed the password for the link.
    link.set_expiry (ip, caller, key, expiry)
    User set an expiration time for the link.
    link.remove_expiry (ip, caller, key)
    User removed the expiration time for the link.
    Link-Based Sharing Parameters
    • caller: UserID of the user who performed the action
    • key: Unique string which forms the URL on which the action is being performed
    • soid: Unique SOID of the object to which the link refers
    • expiry: Number of seconds for which the link will be valid

    Organization Events

    Events related to events that affect an organization
    Organization Events
    org.2fa.level (org, caller, old_level, new_level)
    Admin changed target org's two-factor authentication enforcement level.
    Organization Parameters
    • caller: UserID of the administrator who performed the action
    • org: The pseudo-UserID of the Team Server of the target organization
    • old_level, new_level: The level of two-factor authentication enforcement required for this organization. This will be one of DISALLOWED, OPT_IN, or MANDATORY.

    Type Dictionary

    This section describes some of the object types used by the audit system.

    SOID

    A Store and Object Identifier. This uniquely identifies a single file or folder resource within the AeroFS device network. This object contains two elements:

    sid
    Store ID.
    oid
    Object ID.

    A file (or folder) identifier suitable for use with the AeroFS Content API can be constructed by concatenating sid and oid.

    Sample JSON representation of a SOID:

    {
        "sid": "e4c357190e9d314ca135d6f1f6938d9b",
        "oid": "4e0375957f304b9bbfcddbf87f10b5d2"
    }
    SID

    The SID field uniquely identifies a Store. A Store is a folder container that has an owner and a set of permission. A store could be either the user's root store (i.e. the AeroFS folder) or a shared folder.

    OID

    The OID field represents the Object identifier within a given Store. The OID is an opaque identifier that follows a single file or folder, even across renames. (This is why we do not simply track path names).

    Note that the SID and OID are used to query and modify files and folders using the AeroFS Content API.

    DID

    A Device Identifier is a unique identifier generated for a particular AeroFS device each time it certifies with the AeroFS Private Cloud. If a user unlinks a computer and subsequently re-certifies with AeroFS, that will generate a second DID. To correlate events that report DID to a particular user, search backward for the device.certify event.

    A user can determine the DID for their AeroFS installation. Also note that the Network Diagnostics view reports connected peers by DID. Mobile devices (iOS) and applications that are authorized using the Content API are also assigned a DID.

    Caller

    Identity of the user that authorized a particular action. This is derived from the access token provided with an API call.

    If the access token was issued with administrator scope, the acting_as field will be provided. This field indicates the authority that was used to perform the action. In this case, the email field provides the user that created the access token.

    email
    The user that authorized the action by creating the access token.
    device
    Unique identifier of the app or mobile device that initiated the action.
    acting_as
    If provided, the user ID for whom the access token was granted. Administrators may authorize a privileged access token; in this case the acting_as value will be a specially-formatted administrator id.

    Sample JSON representation of a Caller, followed by a privileged Caller object:

    {
        "email": "jon@aerofs.com",
        "device": "40f9cddab98a432b9c60d734f5f63e7e"
    }
    {
        "email": "jon@aerofs.com",
        "acting_as": ":c34e3c",
        "device": "b88a707bd70586f7a9f2dfc8fee2def5"
    }
    Shared Folder

    A Shared Folder object gives the (device-local) name and SID of a shared folder. Renaming a shared folder will not impact the sid.

    id
    The Store ID for the shared folder
    name
    The name of the Shared Folder at the moment of the auditable event.

    Sample JSON representation of a Shared Folder:

    {
        "id": "35e4a5ffe0c50f8ea5edfc85a1256fab",
        "name": "Team Pictures"
    }
    Path

    A Path object consists of the following elements:

    sid
    Physical root identifier
    relative_path
    Relative path of a file or folder within the given Store.

    Sample JSON representation of a Path:

    {
        "sid": "e4d451120e9d291ca135d6f1f6938d9b",
        "relative_path": "doc/api/overview.pdf"
    }
    Verified Submitter

    The Verified Submitter object will be embedded when the audit event is received over an authenticated link from a certified AeroFS client.

    It will include the following elements:

    user_id
    Reporting user.
    device_id
    Certified device that submitted the auditable event.

    Sample JSON representation of a verified_submitter:

    {
        "user_id": "jon@aerofs.com",
        "device_id": "40f9cddab98a432b9c60d734f5f63e7e"
    }
    Role

    A Role array lists the privileges of a user within a Store. Possible permissions are:

    • MANAGE
    • WRITE

    Sample JSON representation of a Role object:

    {
        "role": ["WRITE"]
    }