Using Items
This section assumes you have already read the overview and understand what collections and items are and what each are for.
Items are very similar to collections in how you interact with them, so this section may look familiar, especially if you just finished the previous one. However, there are some differences, especially when uploading data.
Prerequisite: have a collection
This section assumes you already have a collection created and uploaded. We already covered it in the previous section, but as a reminder:
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const etebase = await Etebase.Account.login("username", "password");
const collectionManager = etebase.getCollectionManager();
const collection = await collectionManager.create("cyberdyne.files",
{
name: "My files",
description: "A collection of files of different types",
},
""
);
await collectionManager.upload(collection);
from etebase import Client, Account
client = Client("client-name")
etebase = Account.login(client, "username", "password")
col_mgr = etebase.get_collection_manager()
# Create, encrypt and upload a new collection
collection = col_mgr.create("cyberdyne.calendar",
{
"name": "Holidays",
"description": "My holiday calendar",
"color": "#23aabbff",
},
"" # Empty content
)
col_mgr.upload(collection)
import com.etebase.client.*;
Client client = Client.create(httpClient, null);
Account etebase = Account.login(client, "username", "password");
CollectionManager colMgr = etebase.getCollectionManager();
// Create, encrypt and upload a new collection
ItemMetadata collectionMetadata = new ItemMetadata();
collectionMetadata.setName("Holidays");
collectionMetadata.setDescription("My holiday calendar");
collectionMetadata.setColor("#23aabbff");
Collection collection = colMgr.create("cyberdyne.calendar", collectionMetadata, "");
colMgr.upload(collection);
import com.etebase.client.*
val client = Client.create(httpClient, null)
val etebase = Account.login(client, "username", "password")
val colMgr = etebase.collectionManager
// Create, encrypt and upload a new collection
collectionMetadata.name = "Holidays"
collectionMetadata.description = "My holiday calendar"
collectionMetadata.color = "#23aabbff"
val collection = colMgr.create("cyberdyne.calendar", collectionMetadata, "")
colMgr.upload(collection)
const char *server_url = etebase_get_default_server_url();
EtebaseClient *client = etebase_client_new("client-name", server_url);
EtebaseAccount *etebase = etebase_account_login(client, "username", "password");
// Create, encrypt and upload a new collection
EtebaseCollectionManager *col_mgr = etebase_account_get_collection_manager(etebase);
EtebaseItemMetadata *col_meta = etebase_collection_metadata_new();
etebase_collection_metadata_set_name(col_meta, "Holidays");
etebase_collection_metadata_set_description(col_meta, "My holiday calendar");
etebase_collection_metadata_set_color(col_meta, "#23aabbff");
EtebaseCollection *col = etebase_collection_manager_create(col_mgr, "cyberdyne.calendar", col_meta, "", 0);
etebase_collection_metadata_destroy(col_meta);
etebase_collection_manager_upload(col_mgr, col, NULL);
// Cleanup
etebase_collection_destroy(col);
etebase_collection_manager_destroy(col_mgr);
use etebase::{Account, Client, ItemMetadata};
let client = Client::new(..., server_url)?;
let etebase = Account::login(client, "username", "password")?;
let collection_manager = etebase.collection_manager()?;
// Create, encrypt and upload a new collection
let collection = collection_manager.create(
"cyberdyne.calendar",
ItemMetadata::new()
.set_name(Some("Holidays"))
.set_description(Some("My holiday calendar"))
.set_color(Some("#23aabbff")),
&[], // Empty content
)?;
collection_manager.upload(&collection, None)?;
Creating items
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
// We can reuse the collection and manager from above
const collectionManager = ...;
const collection = ...;
// Similar to how we have collection manager
const itemManager = collectionManager.getItemManager(collection);
// Create, encrypt and upload a new item
const item = await itemManager.create(
{
type: "file",
name: "note.txt",
mtime: (new Date()).getTime(),
},
"My secret note",
);
await itemManager.batch([item]);
import time
# We can reuse the collection and manager from above
col_mgr = ...
collection = ...
# Similar to how we have a collection manager
item_mgr = col_mgr.get_item_manager(collection)
# Create, encrypt and upload a new item
item = item_mgr.create(
{
"type": "file",
"name": "note.txt",
"mtime": int(round(time.time() * 1000))
},
b"My secret note"
)
item_mgr.batch([item])
CollectionManager colMgr = ...;
Collection collection = ...;
// Similar to how we have collection manager
ItemManager itemManager = colMgr.getItemManager(collection);
// Create, encrypt and upload a new item
ItemMetadata meta = new ItemMetadata();
meta.setType("file");
meta.setName("note.txt");
meta.setMtime((new Date()).getTime());
Item item = itemManager.create(meta, "My secret note");
itemManager.batch(new Item[] {item});
val colMgr = ...
val collection = ...
// Similar to how we have collection manager
val itemManager = colMgr.getItemManager(collection)
// Create, encrypt and upload a new item
val meta = ItemMetadata()
meta.type = "file"
meta.name = "note.txt"
meta.mtime = Date().getTime()
val item = itemManager.create(meta, "My secret note")
itemManager.batch(arrayOf(item))
EtebaseCollectionManager *col_mgr = ...;
EtebaseCollection *col = ...;
// Similar to how we have collection manager
EtebaseItemManager *item_mgr = etebase_collection_manager_get_item_manager(col_mgr, col);
// Create, encrypt and upload a new item
EtebaseItemMetadata *item_meta = etebase_item_metadata_new();
etebase_item_metadata_set_item_type(item_meta, "file");
etebase_item_metadata_set_name(item_meta, "note.txt");
time_t now = time(NULL);
if (now == (time_t)(-1)) {
// Error
}
now *= 1000;
etebase_item_metadata_set_mtime(item_meta, &now);
const char item_content[] = "My secret note";
EtebaseItem *item = etebase_item_manager_create(item_mgr, item_meta, item_content, strlen(item_content));
const EtebaseItem *items[] = { item };
etebase_item_manager_batch(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
etebase_item_destroy(item);
etebase_item_manager_destroy(item_mgr);
let collection_manager = ...
let collection = ...
// Similar to how we have collection manager
let item_manager = collection_manager.item_manager(collection, None)?;
// Create, encrypt and upload a new item
let item = item_manager.create(
ItemMetadata::new()
.set_item_type("files")
.set_name("note.txt"),
b"My secret note",
)?;
item_manager.batch(vec![&item].into_iter(), None)?;
Fetching items
Fetching items is very similar to fetching collections:
Simple fetch
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const itemManager = collectionManager.getItemManager(collection);
const items = await itemManager.list();
/*
Items:
{
data: Etebase.Item[], // Returned array of items
stoken: string, // The sync token for this fetch
... // More fields we'll cover later
}
*/
item_mgr = col_mgr.get_item_manager(collection)
items = item_mgr.list()
# {
# data: iter(etebase.Item), // Returned iterator of items
# stoken: string, // The sync token for this fetch
# ... // More fields we'll cover later
# }
ItemManager itemManager = colMgr.getItemManager(collection);
ItemListResponse items = itemManager.list();
/*
Items:
{
data: Item[], // Returned array of items
stoken: string, // The sync token for this fetch
... // More fields we'll cover later
}
*/
val itemManager = colMgr.getItemManager(collection)
val items = itemManager.list()
/*
Items:
{
data: Item[], // Returned array of items
stoken: string, // The sync token for this fetch
... // More fields we'll cover later
}
*/
EtebaseItemManager *item_mgr = etebase_collection_manager_get_item_manager(col_mgr, col);
EtebaseItemListResponse *item_list = etebase_item_manager_list(item_mgr, NULL);
/*
Items:
{
etebase_item_list_response_get_data(): EtebaseItem[] // Returned array of items
etebase_item_list_response_get_stoken(): const char * // The sync token for this fetch
... // More fields we'll cover later
}
*/
// Cleanup
etebase_item_list_response_destroy(item_list);
etebase_item_manager_destroy(item_mgr);
let item_manager = collection_manager.item_manager(collection)?;
let items = item_manager.list(None)?;
/*
items: ItemListResponse<Item> {
data: Vec<Item>, // Returned array of items
stoken: Option<String>, // The sync token for this fetch
... // More fields we'll cover later
}
*/
The number of returned items is limited by default, and you can control this limit by passing a different limit parameter as we'll see in the next example:
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const items = await itemManager.list({ limit: 50 });
item_mgr = col_mgr.get_item_manager(collection)
items = item_mgr.list(FetchOptions().limit(50))
ItemManager itemManager = colMgr.getItemManager(collection);
ItemListResponse items = itemManager.list(new FetchOptions().limit(50));
val itemManager = colMgr.getItemManager(collection)
val items = itemManager.list(FetchOptions().limit(50))
EtebaseItemManager *item_mgr = etebase_collection_manager_get_item_manager(col_mgr, col);
EtebaseFetchOptions *fetch_options = etebase_fetch_options_new();
etebase_fetch_options_set_limit(fetch_options, 50);
EtebaseItemListResponse *col_list = etebase_item_manager_list(item_mgr, fetch_options);
// Cleanup
etebase_fetch_options_destroy(fetch_options);
etebase_item_list_response_destroy(col_list);
etebase_item_manager_destroy(col_mgr);
let item_manager = collection_manager.item_manager(collection, None)?;
let items = item_manager.list(Some(&FetchOptions::new().limit(50)))?;
Only fetch recent changes
We can use the stoken
we have gotten in previous fetches to only return changed items.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const stoken = "..."; // An stoken we got previously (items.stoken)
const items = await itemManager.list({ stoken });
stoken = "..." # An stoken we got previously e.g. items.stoken
items = item_mgr.list(FetchOptions().stoken(stoken))
String stoken = "..."; // An stoken we got previously (items.stoken)
ItemListResponse items = itemManager.list(new FetchOptions().stoken(stoken));
val stoken = "..." // An stoken we got previously (items.stoken)
val items = itemManager.list(FetchOptions().stoken(stoken))
const char *stoken = "..."; // An stoken we got previously e.g. items.stoken
EtebaseItemManager *item_mgr = etebase_collection_manager_get_item_manager(col_mgr, col);
EtebaseFetchOptions *fetch_options = etebase_fetch_options_new();
etebase_fetch_options_set_stoken(fetch_options, stoken);
EtebaseItemListResponse *col_list = etebase_item_manager_list(item_mgr, NULL);
// Cleanup
etebase_fetch_options_destroy(fetch_options);
etebase_item_list_response_destroy(col_list);
etebase_item_manager_destroy(col_mgr);
let stoken = "..." // An stoken we got previously (items.stoken())
let items = item_manager.list(Some(&FetchOptions::new().limit(stoken)))?;
Fetch in chunks
We can use a combination of limit and stoken to fetch the changes in chunks rather than all at once. This is more resistant to spotty internet connections, and means we can show data to users faster.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
let stoken = null;
while (true) {
const items = await itemManager.list({ stoken, limit: 30 });
stoken = items.stoken;
processNewItems(items.data);
if (items.done) {
break;
}
}
stoken = None
done = False
while not done:
items = item_mgr.list(FetchOptions().stoken(stoken).limit(30))
stoken = items.stoken
done = items.done
process_new_items(items.data)
String stoken = null;
boolean done = false;
while (!done) {
ItemListResponse items = itemManager.list(new FetchOptions().stoken(stoken).limit(30));
stoken = items.getStoken();
done = items.isDone();
process_new_items(items.getData());
}
var stoken: String? = null
var done = false
while (!done) {
val items = itemManager.list(FetchOptions().stoken(stoken).limit(30))
stoken = items.stoken
done = items.isDone
process_new_items(items.data)
}
char *stoken = NULL;
bool done = 0;
while (!done) {
EtebaseFetchOptions *fetch_options = etebase_fetch_options_new();
etebase_fetch_options_set_stoken(fetch_options, stoken);
etebase_fetch_options_set_limit(fetch_options, 30);
EtebaseItemListResponse *item_list = etebase_item_manager_list(item_mgr, fetch_options);
if (stoken) {
free(stoken);
}
stoken = strdup(etebase_item_list_response_get_stoken(item_list));
done = etebase_item_list_response_is_done(item_list);
uintptr_t data_len = etebase_item_list_response_get_data_length(item_list);
const EtebaseItem *data[data_len];
etebase_item_list_response_get_data(item_list, data);
process_new_items(data, data_len);
etebase_fetch_options_destroy(fetch_options);
etebase_item_list_response_destroy(item_list);
}
if (stoken) {
free(stoken);
}
let mut stoken: Option<&str> = None;
let mut items: ItemListResponse<Item>;
loop {
items = item_manager.list(
Some(&FetchOptions::new().stoken(stoken).limit(30))
)?;
stoken = items.stoken();
process_new_items(items.data());
if items.done() {
break
}
}
Fetch by uid
Sometimes we don't care about getting the whole list of items, and we are just interested in fetching one item based on its uid
. We can do it like this:
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const item = await itemManager.fetch(itemUid);
// Can optionally pass stoken to only return the item if changed:
const item = await itemManager.fetch(itemUid, { stoken });
item = item_mgr.fetch(item_uid)
# Can optionally pass stoken to only return the items if changed:
item = item_mgr.fetch(item_uid, FetchOptions().stoken(stoken))
Item item = itemManager.fetch(itemUid);
// Can optionally pass stoken to only return the item if changed:
Item item = itemManager.fetch(itemUid, new FetchOptions().stoken(stoken));
val item = itemManager.fetch(itemUid);
// Can optionally pass stoken to only return the item if changed:
val item = itemManager.fetch(itemUid, FetchOptions().stoken(stoken))
EtebaseItem *item = etebase_item_manager_fetch(item_mgr, item_uid, NULL);
// Can optionally pass stoken to only return the item if changed:
EtebaseFetchOptions *fetch_options = etebase_fetch_options_new();
etebase_fetch_options_set_stoken(fetch_options, stoken);
EtebaseItem *item = etebase_item_manager_fetch(item_mgr, item_uid, fetch_options);
etebase_fetch_options_destroy(fetch_options);
let item = item_manager.fetch(item_uid, None)?;
// Can optionally pass stoken to only return the item if changed:
let item = item_manager.fetch(item_uid, Some(&FetchOptions::new().stoken(stoken)))?;
Fetch multiple by uid
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const items = await itemManager.fetchMulti([item1Uid, item2Uid]);
items = item_mgr.fetch_multi([item1_uid, item2_uid])
ItemListResponse items = itemManager.fetchMulti(new String[] {item1Uid, item2Uid});
val items = itemManager.fetchMulti(arrayOf(item1Uid, item2Uid));
const char *uids[] = { item1_uid, item2_uid };
EtebaseItemListResponse *items = etebase_item_manager_fetch_multi(item_mgr, uids, ETEBASE_UTILS_C_ARRAY_LEN(uids), NULL);
let items = item_manager.fetch_multi(vec![&item1_uid, &item2_uid].into_iter(), None)?;
Fetch a group of items
In addition to fetching all of the changes in a collection, you can also limit the fetching to only a specific subset of items. This is useful, for example, if your data is structured hierarchically (e.g. a directory tree), and you are only interested in refreshing the currently viewed directory.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const toFetch = [item1, item2, item3];
const items = await itemManager.fetchUpdates(toFetch);
// Can optionally pass stoken to limit to new changes
const items = await itemManager.fetchUpdates(toFetch, { stoken });
to_fetch = [item1, item2, item3]
items = item_mgr.fetch_updates(to_fetch)
# Can optionally pass stoken to only return the items if changed:
collection = col_mgr.fetch_updates(to_fetch, FetchOptions().stoken(stoken))
Item[] toFetch = new Item[] {item1, item2, item3};
ItemListResponse items = itemManager.fetchUpdates(to_fetch);
// Can optionally pass stoken to only return the item if changed:
FetchOptions fetchOptions = new FetchOptions().stoken(stoken);
ItemListResponse items = itemManager.fetchUpdates(to_fetch, fetch_options);
val toFetch = arrayOf(item1, item2, item3)
val items = itemManager.fetchUpdates(to_fetch)
// Can optionally pass stoken to only return the item if changed:
val fetchOptions = FetchOptions().stoken(stoken)
val items = itemManager.fetchUpdates(to_fetch, fetch_options)
const EtebaseItem *items[] = { item1, item2, item3 };
EtebaseItemListResponse *item_list = etebase_item_manager_fetch_updates(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
// Cleanup
etebase_item_list_response_destroy(item_list);
// Can optionally pass stoken to only return the item if changed:
EtebaseFetchOptions *fetch_options = etebase_fetch_options_new();
etebase_fetch_options_set_stoken(fetch_options, stoken);
EtebaseItemListResponse *item_list = etebase_item_manager_fetch_updates(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), fetch_options);
// Cleanup
etebase_fetch_options_destroy(fetch_options);
etebase_item_list_response_destroy(item_list);
let to_fetch = vec![&item1, &item2, &item3];
let items = item_manager.fetch_updates(to_fetch.into_iter(), None)?;
// Can optionally pass stoken to only return the item if changed:
let fetch_options = FetchOptions::new().stoken(stoken);
let items = item_manager.fetch_updates(to_fetch.into_iter(), Some(&fetch_options))?;
Modifying and deleting items
Modifying items is easy, it's just a matter of changing them and uploading them.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
await item.setContent("new secret content");
await itemManager.batch([item]);
item.content = "new secret content"
item_mgr.batch([item])
ItemMetadata itemMeta = item.getMeta();
itemMeta.setName("Name");
item.setMeta(itemMeta);
itemManager.batch(new Item[] {item});
val itemMeta = item.meta
itemMeta.name = "Name"
item.meta = itemMeta
itemManager.batch(arrayOf(item))
EtebaseItemMetadata *item_meta = etebase_item_get_meta(item);
etebase_item_metadata_set_name(item_meta, "Name");
etebase_item_set_meta(item, item_meta);
etebase_item_metadata_destroy(item_meta);
const EtebaseItem *items[] = { item };
etebase_item_manager_batch(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
let mut item_meta = item.meta()?;
item_meta.set_name("Name");
item.set_meta(&item_meta)?;
item_manager.batch(vec![&item].into_iter(), None)?;
Deleting is even easier:
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
item.delete();
await itemManager.batch([item]);
item.delete()
item_mgr.batch([item])
item.delete();
itemManager.batch(new Item[] {item});
item.delete()
itemManager.batch(arrayOf(item))
etebase_item_delete(item);
const EtebaseItem *items[] = { item };
etebase_item_manager_batch(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
item.delete()?;
item_manager.batch(vec![&item].into_iter(), None)?;
Uploading multiple items
As you saw in the previous examples, unlike the collection's upload
, batch
accepts an array of items. This can be used for uploading multiple items at once:
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
await itemManager.batch([item1, item2, item3, ...]);
item_mgr.batch([item1, item2, item3, ...])
itemManager.batch(new Item[] {item1, item2, item3, ...});
itemManager.batch(arrayOf(item1, item2, item3, ...))
const EtebaseItem *items[] = { item1, item2, item3, ... };
etebase_item_manager_batch(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
item_manager.batch(vec![&item1, &item2, &item3, ...].into_iter(), None)?;
Advanced uploads and transactions
In the examples above we always uploaded the items in a way that overwrote whatever is on the server, regardless if it has changed since we last fetched it, or not. While this is fine in many cases, in some cases you want to prevent that in order to ensure the consistency of data.
Transactions
The easiest way to ensure consistency is just to use transactions. Transactions make sure that what we think is the most recent version, really is, and will fail otherwise. This consistency check is done across all of the items, and if one item fails the check, the whole transaction fails.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
// -> On device A:
const item1 = await.itemManager.fetch(itemUid1);
const item2 = await.itemManager.fetch(itemUid2);
// -> On device B:
const item1 = await.itemManager.fetch(itemUid1);
await item1.setContent("something else for item 1");
await itemManager.batch([item1]);
// -> On device A (using the previously saved item)
await item1.setContent("new content for item 1");
await item2.setContent("new content for item 2");
// Will fail because item1 changed on device B
await itemManager.transaction([item1, item2]);
// Will succeed
await itemManager.batch([item1, item2]);
// Will succeed because item2 hasn't changed on device B
await itemManager.transaction([item2]);
# -> On device A:
item1 = item_mgr.fetch(item_uid1)
item2 = item_mgr.fetch(item_uid2)
# -> On device B:
item1 = item_mgr.fetch(item_uid1)
item1.content = b"something else for item 1"
item_mgr.batch([item1])
# -> On device A (using the previously saved item)
item1.content = b"new content for item 1"
item2.content = b"new content for item 2"
# Will fail because item1 changed on device B
item_mgr.transaction([item1, item2])
# Will succeed
item_mgr.batch([item1, item2])
# Will succeed because item2 hasn't changed on device B
item_mgr.transaction([item2])
// -> On device A:
Item item1 = itemManager.fetch(itemUid1);
Item item2 = itemManager.fetch(itemUid2);
// -> On device B:
Item item1 = itemManager.fetch(itemUid1);
item1.setContent("Something else for item 1")
itemManager.batch(new Item[] {item1});
// -> On device A (using the previously saved item)
item1.setContent("New content for item 1");
item2.setContent("New content for item 2");
// Will fail because item1 changed on device B
itemManager.transaction(new Item[] {item1, item2});
// Will succeed
itemManager.batch(new Item[] {item1, item2});
// Will succeed because item2 hasn't changed on device B
itemManager.transaction(new Item[] {item2});
// -> On device A:
val item1 = itemManager.fetch(itemUid1)
val item2 = itemManager.fetch(itemUid2)
// -> On device B:
val item1 = itemManager.fetch(itemUid1)
item1.content = "Something else for item 1"
itemManager.batch(arrayOf(item1))
// -> On device A (using the previously saved item)
item1.content = "New content for item 1"
item2.content = "New content for item 2"
// Will fail because item1 changed on device B
itemManager.transaction(arrayOf(item1, item2))
// Will succeed
itemManager.batch(arrayOf(item1, item2))
// Will succeed because item2 hasn't changed on device B
itemManager.transaction(arrayOf(item2))
// -> On device A:
EtebaseItem *item1 = etebase_item_manager_fetch(item_mgr, item1_uid, NULL);
EtebaseItem *item2 = etebase_item_manager_fetch(item_mgr, item2_uid, NULL);
// -> On device B:
EtebaseItem *item1 = etebase_item_manager_fetch(item_mgr, item1_uid, NULL);
const char tmp[] = "Something else for item 1";
etebase_item_set_content(item1, tmp, strlen(tmp));
const EtebaseItem *items[] = { item1 };
etebase_item_manager_batch(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
etebase_item_destroy(item1);
// -> On device A (using the previously saved item)
const char tmp[] = "New content for item 1";
etebase_item_set_content(item1, tmp, strlen(tmp));
const char tmp2[] = "New content for item 2";
etebase_item_set_content(item2, tmp2, strlen(tmp2));
// Will fail because item1 changed on device B
const EtebaseItem *items[] = { item1, item2 };
etebase_item_manager_transaction(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
// Will succeed
etebase_item_manager_batch(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
// Will succeed because item2 hasn't changed on device B
const EtebaseItem *items2[] = { item2 };
etebase_item_manager_transaction(item_mgr, items2, ETEBASE_UTILS_C_ARRAY_LEN(items2), NULL);
etebase_item_destroy(item2);
etebase_item_destroy(item1);
// -> On device A:
let mut item1 = item_manager.fetch(item_uid1, None)?;
let mut item2 = item_manager.fetch(item_uid2, None)?;
// -> On device B:
let mut item1 = item_manager.fetch(item_uid1, None)?;
item1.set_content(b"Something else for item 1")?;
item_manager.batch(vec![&item1].into_iter(), None)?;
// -> On device A (using the previously saved item)
item1.set_content(b"New content for item 1")?;
item2.set_content(b"New content for item 2")?;
// Will fail because item1 changed on device B
item_manager.transaction(vec![&item1, &item2].into_iter(), None)?;
// Will succeed
item_manager.batch(vec![&item1, &item2].into_iter(), None)?;
// Will succeed because item2 hasn't changed on device B
item_manager.transaction(vec![&item2].into_iter(), None)?;
Using stoken
Like with collections, transactions will only fail if the items themselves have changed, but will not fail if another item of the collection has changed. In some cases we want to have collection-wide consistency and want to make sure nothing has changed.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
// -> On device A:
const stoken = collection.stoken;
const item = await.itemManager.fetch(itemUid);
// -> On device B:
const anotherItem = await.itemManager.fetch(anotherItemUid);
await anotherItem.setContent("content for another item");
await itemManager.batch([anotherItem]);
// -> On device A (using the previously saved items and stoken)
await item.setContent("new secret content");
// Both will fail
await itemManager.transaction([item], null, { stoken });
await itemManager.batch([item], null, { stoken });
// Both will succeed
await itemManager.transaction([item]);
await itemManager.batch([item]);
# -> On device A:
stoken = collection.stoken
item = item_mgr.fetch(item_uid)
# -> On device B:
another_item = item_mgr.fetch(another_item_uid)
another_item.content = b"content for another item"
item_mgr.batch([another_item])
# -> On device A (using the previously saved item and stoken)
item.content = b"new secret content"
# Both will fail
fetch_options = FetchOptions().stoken(stoken)
item_mgr.transaction([item], fetch_options=fetch_options)
item_mgr.batch([item], fetch_options=fetch_options)
# Both will succeed
item_mgr.transaction([item])
item_mgr.batch([item])
// -> On device A:
String stoken = collection.stoken
Item item = itemManager.fetch(itemUid);
// -> On device B:
Item anotherItem = itemManager.fetch(anotherItemUid);
anotherItem.setContent("content for another item")
itemManager.batch(new Item[] {anotherItem});
// -> On device A (using the previously saved item and stoken)
item.setContent("new secret content")
// Both will fail
FetchOptions fetchOptions = new FetchOptions().stoken(stoken);
itemManager.transaction(new Item[] {item}, null, fetchOptions);
itemManager.batch(new Item[] {item}, null, fetchOptions);
// Both will succeed
itemManager.transaction(new Item[] {item});
itemManager.batch(new Item[] {item});
// -> On device A:
val stoken = collection.stoken
val item = itemManager.fetch(itemUid)
// -> On device B:
val anotherItem = itemManager.fetch(anotherItemUid)
anotherItem.content = "content for another item"
itemManager.batch(arrayOf(anotherItem))
// -> On device A (using the previously saved item and stoken)
item.content = "new secret content"
// Both will fail
val fetchOptions = FetchOptions().stoken(stoken)
itemManager.transaction(arrayOf(item), null, fetchOptions)
itemManager.batch(arrayOf(item), null, fetchOptions)
// Both will succeed
itemManager.transaction(arrayOf(item))
itemManager.batch(arrayOf(item))
// -> On device A:
EtebaseItem *item = etebase_item_manager_fetch(item_mgr, item_uid, NULL);
const char *stoken = etebase_collection_get_stoken(col);
{
// -> On device B:
EtebaseItem *another_item = etebase_item_manager_fetch(item_mgr, another_item_uid, NULL);
const char tmp[] = "content for another item";
etebase_item_set_content(item1, tmp, strlen(tmp));
const EtebaseItem *items[] = { another_item };
etebase_item_manager_batch(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
etebase_item_destroy(another_item);
}
// -> On device A (using the previously saved item and stoken)
const char tmp[] = "new secret content";
etebase_item_set_content(item, tmp, strlen(tmp));
// Both will fail
EtebaseFetchOptions *fetch_options = etebase_fetch_options_new();
etebase_fetch_options_set_stoken(fetch_options, stoken);
const EtebaseItem *items[] = { item };
etebase_item_manager_transaction(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), fetch_options);
etebase_item_manager_batch(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), fetch_options);
etebase_fetch_options_destroy(fetch_options);
// Both will succeed
etebase_item_manager_transaction(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
etebase_item_manager_batch(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
etebase_item_destroy(item);
// -> On device A:
let stoken = collection.stoken();
let mut item = item_manager.fetch(item_uid, None)?;
// -> On device B:
let mut another_item = item_manager.fetch(another_item_uid, None)?;
another_item.set_content(b"content for another item")?;
item_manager.batch(vec![&another_item].into_iter(), None)?;
// -> On device A (using the previously saved item and stoken)
item.set_content(b"new secret content")?;
// Both will fail
let fetch_options = FetchOptions::new().stoken(stoken);
item_manager.transaction(vec![&item].into_iter(), Some(&fetch_options))?;
item_manager.batch(vec![&item].into_iter(), Some(&fetch_options))?;
Both will
// Both will succeed
item_manager.transaction(vec![&item].into_iter(), None)?;
item_manager.batch(vec![&item].into_iter(), None)?;
Additional dependencies
Sometimes we may want a transaction
or batch
upload to fail if some items have changed but not upload them. These are called dependencies and can be passed to both transactions and batches.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
// -> On device A:
const item1 = await.itemManager.fetch(itemUid1);
const item2 = await.itemManager.fetch(itemUid2);
// -> On device B:
const item1 = await.itemManager.fetch(itemUid1);
await item1.setContent("something else for item 1");
await itemManager.batch([item1]);
// -> On device A (using the previously saved items and stoken)
await item2.setContent("new secret content");
// Both will fail because item1 changed
await itemManager.transaction([item2], [item1]);
await itemManager.batch([item2], [item1]);
// Can even use the item in both the list and deps in batch
// Will fail because item1 changed on device B
await itemManager.batch([item1, item2], [item1]);
# -> On device A:
item1 = item_mgr.fetch(item_uid1)
item2 = item_mgr.fetch(item_uid2)
# -> On device B:
item1 = item_mgr.fetch(item_uid1)
item1.content = b"something else for item 1"
item_mgr.batch([item1])
# -> On device A (using the previously saved item)
item2.content = b"new content for item 2"
# Both will fail because item1 changed
item_mgr.transaction([item2], [item1])
item_mgr.batch([item2], [item1])
# Can even use the item in both the list and deps in batch
# Will fail because item1 changed on device B
item_mgr.batch([item1, item2], [item1])
// -> On device A:
Item item1 = itemManager.fetch(itemUid1);
Item item2 = itemManager.fetch(itemUid2);
// -> On device B:
Item item1 = itemManager.fetch(itemUid1);
item1.setContent("Something else for item1");
itemManager.batch(new Item[] {item1});
// -> On device A (using the previously saved collection)
item2.setContent("New content for item 2");
// Both will fail because item1 changed
itemManager.transaction(new Item[] {item2}, new Item[] {item1});
itemManager.batch(new Item[] {item2}, new Item[] {item1});
// Can even use the item in both the list and deps in batch
// Will fail because item1 changed on device B
itemManager.batch(new Item[] {item1, item2}, new Item[] {item1});
// -> On device A:
val item1 = itemManager.fetch(itemUid1)
val item2 = itemManager.fetch(itemUid2)
// -> On device B:
val item1 = itemManager.fetch(itemUid1)
item1.content = "Something else for item1"
itemManager.batch(arrayOf(item1))
// -> On device A (using the previously saved collection)
item2.content = "New content for item 2"
// Both will fail because item1 changed
itemManager.transaction(arrayOf(item2), arrayOf(item1));
itemManager.batch(arrayOf(item2), arrayOf(item1));
// Can even use the item in both the list and deps in batch
// Will fail because item1 changed on device B
itemManager.batch(arrayOf(item1, item2), arrayOf(item1));
// -> On device A:
EtebaseItem *item1 = etebase_item_manager_fetch(item_mgr, item1_uid, NULL);
EtebaseItem *item2 = etebase_item_manager_fetch(item_mgr, item2_uid, NULL);
// -> On device B:
EtebaseItem *item1 = etebase_item_manager_fetch(item_mgr, item1_uid, NULL);
const char tmp[] = "Something else for item 1";
etebase_item_set_content(item1, tmp, strlen(tmp));
const EtebaseItem *items[] = { item1 };
etebase_item_manager_batch(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
etebase_item_destroy(item1);
// -> On device A (using the previously saved collection)
const char tmp2[] = "New content for item 2";
etebase_item_set_content(item2, tmp2, strlen(tmp2));
// Both will fail because item1 changed
const EtebaseItem *items[] = { item2 };
const EtebaseItem *deps[] = { item1 };
etebase_item_manager_batch_deps(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), deps, ETEBASE_UTILS_C_ARRAY_LEN(deps), NULL);
etebase_item_manager_transaction_deps(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), deps, ETEBASE_UTILS_C_ARRAY_LEN(deps), NULL);
// Can even use the item in both the list and deps in batch
// Will fail because item1 changed on device B
const EtebaseItem *items[] = { item1, item2 };
const EtebaseItem *deps[] = { item1 };
etebase_item_manager_batch_deps(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), deps, ETEBASE_UTILS_C_ARRAY_LEN(deps), NULL);
etebase_item_destroy(item2);
etebase_item_destroy(item1);
// -> On device A:
let mut item1 = item_manager.fetch(item_uid1, None)?;
let mut item2 = item_manager.fetch(item_uid2, None)?;
// -> On device B:
let mut item1 = item_manager.fetch(item_uid1, None)?;
item1.set_content(b"Something else for item1")?;
item_manager.batch(vec![&item1].into_iter(), None)?;
// -> On device A (using the previously saved collection)
item2.set_content(b"New content for item 2")?;
// Both will fail because item1 changed
item_manager.transaction_deps(vec![&item2].into_iter(), vec![&item1].into_iter(), None)?;
item_manager.batch_deps(vec![&item2].into_iter(), vec![&item1].into_iter(), None)?;
// Can even use the item in both the list and deps in batch
// Will fail because item1 changed on device B
item_manager.batch_deps(vec![&item1, &item2].into_iter(), vec![&item1].into_iter(), None)?;
Treating collections as items
In the examples above we only covered consistency of items, but what happens if we want to ensure the collection itself is consistent with its items? One case where this is useful, is if your data is ordered hierarchically (e.g. as a tree) with the collection as the root. In this case, you will want to be able to create a child and have it added to the root node in the same transaction.
In Etebase, collections are essentially just items with some extra data, so you can use collections directly as items with just a small difference.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
// A pre-existing collection and a few items:
const collection = ...;
const item1 = ...;
const item2 = ...;
// Get the item out of the collection
const colItem = collection.item;
// The collection item can then be used like any other item:
await itemManager.transaction([colItem, item1], [item2]);
await itemManager.transaction([item1, item2], [colItem]);
await itemManager.batch([colItem, item1]);
// In addition, these are true:
assert(collection.getMeta() === colItem.getMeta());
assert((await collection.getContent()) === (await colItem.getContent()));
# A pre-existing collection and a few items:
collection = ...
item1 = ...
item2 = ...
# Get the item out of the collection
col_item = collection.item;
# The collection item can then be used like any other item:
item_mgr.transaction([col_item, item1], [item2]);
item_mgr.transaction([item1, item2], [col_item]);
item_mgr.batch([col_item, item1]);
# In addition, these are true:
assert(collection.meta == col_item.meta)
assert(collection.content == col_item.content)
Collection collection = ...;
Item item1 = ...;
Item item2 = ...;
// Get the item out of the collection
Item colItem = collection.asItem();
// The collection item can then be used like any other item:
itemManager.transaction(new Item[] {colItem, item1}, new Item[] {item2});
itemManager.transaction(new Item[] {item1, item2}, new Item[] {colItem});
itemManager.batch(new Item[] {colItem, item1});
// In addition, these are true:
assert (collection.getMetaRaw() == colItem.getMetaRaw());
assert (collection.getContent() == colItem.getContent());
val collection = ...
val item1 = ...
val item2 = ...
// Get the item out of the collection
var colItem = collection.asItem()
// The collection item can then be used like any other item:
itemManager.transaction(arrayOf(colItem, item1), arrayOf(item2)
itemManager.transaction(arrayOf(item1, item2), arrayOf(colItem))
itemManager.batch(arrayOf(colItem, item1))
// In addition, these are true:
assert(collection.metaRaw == colItem.metaRaw)
assert(collection.content == colItem.content)
// Get the item out of the collection
EtebaseItem *col_item = etebase_collection_as_item(col);
// The collection item can then be used like any other item:
const EtebaseItem *items[] = { col_item, item1 };
const EtebaseItem *deps[] = { item2 };
etebase_item_manager_transaction_deps(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items),
deps, ETEBASE_UTILS_C_ARRAY_LEN(deps), NULL);
const EtebaseItem *items[] = { item1, item2 };
const EtebaseItem *deps[] = { col_item };
etebase_item_manager_transaction_deps(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items),
deps, ETEBASE_UTILS_C_ARRAY_LEN(deps), NULL);
const EtebaseItem *items[] = { col_item, item1 };
etebase_item_manager_batch(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
// In addition, these are true:
// etebase_collection_get_meta_raw(col, ...) == etebase_item_get_meta_raw(col_item, ...)
// etebase_collection_get_content(col, ...) == etebase_item_get_content(col_item, ...)
let collection = ...
let item1 = ...
let item2 = ...
// Get the item out of the collection
let col_item = collection.item()?;
// The collection item can then be used like any other item:
item_manager.transaction_deps(vec![&col_item, &item1].into_iter(), vec![&item2].into_iter(), None)?;
item_manager.transaction_deps(vec![&item1, &item2].into_iter(), vec![&col_item].into_iter(), None)?;
item_manager.batch(vec![&col_item, &item1].into_iter(), None)?;
// In addition, these are true:
assert_eq!(collection.meta_raw()?, col_item.meta_raw()?);
assert_eq!(collection.content()?, col_item.content()?);
You can also fetch the collection as an item from all of the item API functions:
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const itemManager = collectionManager.getItemManager(collection);
// Will return the collection item as part of the list:
const items = await itemManager.list({ withCollection: true });
// Assuming the collection is the first item returned:
const colItem = items.data[0];
assert(colItem.uid === collection.uid);
// You can also fetch collection items based on UID:
const colItem = await itemManager.fetch(collection.uid);
// Or fetch updates of the collection along with other items:
const items = await itemManager.fetchUpdates([collection.item, item1]);
item_mgr = col_mgr.get_item_manager(collection)
# Will return the collection item as part of the list:
fetch_options = FetchOptions().with_collection(true)
items = item_mgr.list(fetch_options);
# Assuming the collection is the first item returned:
col_item = items.data[0];
assert(col_item.uid == collection.uid)
# You can also fetch collection items based on UID:
col_item = item_mgr.fetch(collection.uid);
# Or fetch updates of the collection along with other items:
items = item_mgr.fetch_updates([collection.item, item1]);
ItemManager itemManager = colMgr.getItemManager(collection);
ItemListResponse items = itemManager.list(new FetchOptions().with_collection(true));
// Assuming the collection is the first item returned:
Item colItem = items.getData()[0];
assert (colItem.uid === collection.uid);
// You can also fetch collection items based on UID:
Item colItem = itemManager.fetch(collection.uid);
// Or fetch updates of the collection along with other items:
ItemListResponse items = itemManager.fetchUpdates(new Item[] { collection.asItem(), item1 });
val itemManager = colMgr.getItemManager(collection)
val items = itemManager.list(FetchOptions().with_collection(true))
// Assuming the collection is the first item returned:
val colItem = items.getData()[0]
assert(colItem.uid === collection.uid)
// You can also fetch collection items based on UID:
val colItem = itemManager.fetch(collection.uid)
// Or fetch updates of the collection along with other items:
val items = itemManager.fetchUpdates(arrayOf(collection.asItem(), item1))
EtebaseItemManager *item_mgr = etebase_collection_manager_get_item_manager(col_mgr, col);
EtebaseFetchOptions *fetch_options = etebase_fetch_options_new();
etebase_fetch_options_set_with_collection(fetch_options, true);
etebase_fetch_options_set_limit(fetch_options, 1);
EtebaseItemListResponse *item_list = etebase_item_manager_list(item_mgr, fetch_options);
etebase_fetch_options_destroy(fetch_options);
// Assuming the collection is the first item returned:
const EtebaseItem *list_items[1];
etebase_item_list_response_get_data(item_list, list_items);
EtebaseItem *col_item = list_items[0];
// This is true:
// etebase_item_get_uid(item) == etebase_collection_get_uid(col)
// You can also fetch collection items based on UID:
EtebaseItem *col_item = etebase_item_manager_fetch(item_mgr, col_uid, NULL);
// Or fetch updates of the collection along with other items:
const EtebaseItem *items[] = { col_item, item1 };
EtebaseItemListResponse *item_list = etebase_item_manager_fetch_updates(item_mgr, items, ETEBASE_UTILS_C_ARRAY_LEN(items), NULL);
let item_manager = collection_manager.item_manager(&collection)?;
let items = item_manager.list(Some(&FetchOptions::new().with_collection(true)))?;
// Assuming the collection is the first item returned:
let col_item = &items.data()[0];
assert_eq!(col_item.uid(), collection.uid());
// You can also fetch collection items based on UID:
let col_item = item_manager.fetch(collection.uid(), None)?;
// Or fetch updates of the collection along with other items:
let items = item_manager.fetch_updates(vec![&collection.item()?, &item1].into_iter(), None);
Subscriptions (live updates)
Some applications are interactive in nature and their data changes often. For these applications it's useful to be able to subscribe to live-updates so your app gets notified the moment data is changed. This is what live updates are for.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const subscription = await itemMgr.subscribeChanges((items) => {
/*
items:
{
data: Etebase.Item[], // Returned array of items
stoken: string, // The sync token for this fetch
... // More fields we'll cover later
}
This is the same as the list response above.
*/
});
// Unsubscribe from updates:
subscription.unsubscribe();
Subscriptions are only supported in JavaScript at the moment. Please open a ticket if you would like to see it implemented.
Subscriptions are only supported in JavaScript at the moment. Please open a ticket if you would like to see it implemented.
Subscriptions are only supported in JavaScript at the moment. Please open a ticket if you would like to see it implemented.
Subscriptions are only supported in JavaScript at the moment. Please open a ticket if you would like to see it implemented.
Subscriptions are only supported in JavaScript at the moment. Please open a ticket if you would like to see it implemented.
Binary content
In the examples above content was always a string. However, content is actually a binary blob of data, not a string. Using it as a string is just a convenience.
Here is how you can control the formatting of the data:
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
// default, returns a Uint8Array
item.getContent();
// tries to convert the binary data to a string and returns that
item.getContent(Etebase.OutputFormat.String);
// Sets the content to a binary blob
item.setContent(Uint8Array.from([72, 101, 108, 108, 111]));
// Sets the content to a string
item.setContent("Hello");
# The Python API returns `bytes` by default
item.content
# Try decoding the binary data to a UTF-8 string
item.content.decode()
# Sets the content to some byte array
item.content = b"Some bytes"
# Set the content to a string
item.content = "Some bytes".encode()
// The Java API returns `byte[]` by default
item.getContent();
// Try decoding the binary data to a UTF-8 string and return that
item.getContentString();
// Sets the content to a binary blob
item.setContent("Bla".getBytes("UTF-8"));
// Sets the content to a string
item.setContent("Hello");
// The Java API returns `byte[]` by default
item.content
// Try decoding the binary data to a UTF-8 string and return that
item.contentString
// Sets the content to a binary blob
item.content = "Bla".toByteArray()
// Sets the content to a string
item.content = "Hello"
// The C API always works on binary data,
// This means content is *not* NULL terminated!
// It's generally fastest to just try to get the content into a buffer
// if we think it's likely to be under a certain size.
char tmp[500];
intptr_t len = etebase_item_get_content(item, tmp, sizeof(tmp));
if (len < 0) {
// Error
} else if (len > sizeof(tmp)) {
// This means we should allocate a buffer of size len to fetch into
char *tmp2 = malloc(len);
etebase_item_get_content(item, tmp2, len);
// Do something with tmp2
free(tmp2);
}
// As said above, content is not null terminated.
// To get the content as a null terminated string:
char tmp[500];
intptr_t len = etebase_item_get_content(item, tmp, sizeof(tmp));
if (len < 0) {
// Error
} else {
tmp[len] = 0;
}
// The rust API returns `Vec<u8>` by default
item.content()?;
// Try decoding the binary data to a UTF-8 string and return that
String::from_utf8(item.content()?);
// Sets the content to a binary blob
item.set_content(b"Bla")?;
// Sets the content from a string
item.set_content("Hello".as_bytes())?;