storage/stream: Add persistent write progress to stream_flash

Add additional API to stream_flash that can be used to make
stream write progress persistent using the settings subsystem.
This functionality makes it possible to resume a write operation
after it was interrupted, e.g. by power loss.

Signed-off-by: Jonathan Nilsen <Jonathan.Nilsen@nordicsemi.no>
diff --git a/doc/reference/storage/stream/stream_flash.rst b/doc/reference/storage/stream/stream_flash.rst
index 808767b..6162388 100644
--- a/doc/reference/storage/stream/stream_flash.rst
+++ b/doc/reference/storage/stream/stream_flash.rst
@@ -17,6 +17,17 @@
 other operations, such as radio RX and TX. Also, fewer write operations result
 in faster response times seen from the application.
 
+Persistent stream write progress
+********************************
+Some stream write operations, such as DFU operations, may run for a long time.
+When performing such long running operations it can be useful to be able to save
+the stream write progress to persistent storage so that the operation can resume
+at the same point after an unexpected interruption.
+
+The Stream Flash module offers an API for loading, saving and clearing stream
+write progress to persistent storage using the :ref:`Settings <settings_api>`
+module. The API can be enabled using :option:`CONFIG_STREAM_FLASH_PROGRESS`.
+
 API Reference
 *************
 
diff --git a/include/storage/stream_flash.h b/include/storage/stream_flash.h
index bc80bf1..1a928fa 100644
--- a/include/storage/stream_flash.h
+++ b/include/storage/stream_flash.h
@@ -128,6 +128,47 @@
  */
 int stream_flash_erase_page(struct stream_flash_ctx *ctx, off_t off);
 
+/**
+ * @brief Load persistent stream write progress stored with key
+ *        @p settings_key .
+ *
+ * This function should be called directly after @ref stream_flash_init to
+ * load previous stream write progress before writing any data. If the loaded
+ * progress has fewer bytes written than @p ctx then it will be ignored.
+ *
+ * @param ctx context
+ * @param settings_key key to use with the settings module for loading
+ *                     the stream write progress
+ *
+ * @return non-negative on success, negative errno code on fail
+ */
+int stream_flash_progress_load(struct stream_flash_ctx *ctx,
+			       const char *settings_key);
+
+/**
+ * @brief Save persistent stream write progress using key @p settings_key .
+ *
+ * @param ctx context
+ * @param settings_key key to use with the settings module for storing
+ *                     the stream write progress
+ *
+ * @return non-negative on success, negative errno code on fail
+ */
+int stream_flash_progress_save(struct stream_flash_ctx *ctx,
+			       const char *settings_key);
+
+/**
+ * @brief Clear persistent stream write progress stored with key
+ *        @p settings_key .
+ *
+ * @param ctx context
+ * @param settings_key key previously used for storing the stream write progress
+ *
+ * @return non-negative on success, negative errno code on fail
+ */
+int stream_flash_progress_clear(struct stream_flash_ctx *ctx,
+				const char *settings_key);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/subsys/storage/stream/Kconfig b/subsys/storage/stream/Kconfig
index 63a2420..63a9df4 100644
--- a/subsys/storage/stream/Kconfig
+++ b/subsys/storage/stream/Kconfig
@@ -17,6 +17,15 @@
 	  If disabled an external actor must erase the flash area being written
 	  to.
 
+config STREAM_FLASH_PROGRESS
+	bool "Persistent stream write progress"
+	depends on SETTINGS
+	depends on !SETTINGS_NONE
+	help
+	  Enable API for loading and storing the current write progress to flash
+	  using the settings subsystem. In case of power failure or device
+	  reset, the API can be used to resume writing from the latest state.
+
 module = STREAM_FLASH
 module-str = stream flash
 source "subsys/logging/Kconfig.template.log_config"
diff --git a/subsys/storage/stream/stream_flash.c b/subsys/storage/stream/stream_flash.c
index 5c5a99d..55a10f5 100644
--- a/subsys/storage/stream/stream_flash.c
+++ b/subsys/storage/stream/stream_flash.c
@@ -16,6 +16,62 @@
 
 #include <storage/stream_flash.h>
 
+#ifdef CONFIG_STREAM_FLASH_PROGRESS
+#include <settings/settings.h>
+
+static int settings_direct_loader(const char *key, size_t len,
+				  settings_read_cb read_cb, void *cb_arg,
+				  void *param)
+{
+	struct stream_flash_ctx *ctx = (struct stream_flash_ctx *) param;
+
+	/* Handle the subtree if it is an exact key match. */
+	if (settings_name_next(key, NULL) == 0) {
+		size_t bytes_written = 0;
+		ssize_t len = read_cb(cb_arg, &bytes_written,
+				      sizeof(bytes_written));
+
+		if (len != sizeof(ctx->bytes_written)) {
+			LOG_ERR("Unable to read bytes_written from storage");
+			return len;
+		}
+
+		/* Check that loaded progress is not outdated. */
+		if (bytes_written >= ctx->bytes_written) {
+			ctx->bytes_written = bytes_written;
+		} else {
+			LOG_WRN("Loaded outdated bytes_written %zu < %zu",
+				bytes_written, ctx->bytes_written);
+			return 0;
+		}
+
+#ifdef CONFIG_STREAM_FLASH_ERASE
+		int rc;
+		struct flash_pages_info page;
+		off_t offset = (off_t) (ctx->offset + ctx->bytes_written) - 1;
+
+		/* Update the last erased page to avoid deleting already
+		 * written data.
+		 */
+		if (ctx->bytes_written > 0) {
+			rc = flash_get_page_info_by_offs(ctx->fdev, offset,
+							 &page);
+			if (rc != 0) {
+				LOG_ERR("Error %d while getting page info", rc);
+				return rc;
+			}
+			ctx->last_erased_page_start_offset = page.start_offset;
+		} else {
+			ctx->last_erased_page_start_offset = -1;
+		}
+#endif /* CONFIG_STREAM_FLASH_ERASE */
+	}
+
+	return 0;
+}
+
+#endif /* CONFIG_STREAM_FLASH_PROGRESS */
+
 #ifdef CONFIG_STREAM_FLASH_ERASE
 
 int stream_flash_erase_page(struct stream_flash_ctx *ctx, off_t off)
@@ -201,6 +257,15 @@
 		return -EFAULT;
 	}
 
+#ifdef CONFIG_STREAM_FLASH_PROGRESS
+	int rc = settings_subsys_init();
+
+	if (rc != 0) {
+		LOG_ERR("Error %d initializing settings subsystem", rc);
+		return rc;
+	}
+#endif
+
 	struct _inspect_flash inspect_flash_ctx = {
 		.buf_len = buf_len,
 		.total_size = 0
@@ -241,3 +306,62 @@
 
 	return 0;
 }
+
+#ifdef CONFIG_STREAM_FLASH_PROGRESS
+
+int stream_flash_progress_load(struct stream_flash_ctx *ctx,
+			       const char *settings_key)
+{
+	if (!ctx || !settings_key) {
+		return -EFAULT;
+	}
+
+	int rc = settings_load_subtree_direct(settings_key,
+					      settings_direct_loader,
+					      (void *) ctx);
+
+	if (rc != 0) {
+		LOG_ERR("Error %d while loading progress for \"%s\"",
+			rc, settings_key);
+	}
+
+	return rc;
+}
+
+int stream_flash_progress_save(struct stream_flash_ctx *ctx,
+			       const char *settings_key)
+{
+	if (!ctx || !settings_key) {
+		return -EFAULT;
+	}
+
+	int rc = settings_save_one(settings_key,
+				   &ctx->bytes_written,
+				   sizeof(ctx->bytes_written));
+
+	if (rc != 0) {
+		LOG_ERR("Error %d while storing progress for \"%s\"",
+			rc, settings_key);
+	}
+
+	return rc;
+}
+
+int stream_flash_progress_clear(struct stream_flash_ctx *ctx,
+				const char *settings_key)
+{
+	if (!ctx || !settings_key) {
+		return -EFAULT;
+	}
+
+	int rc = settings_delete(settings_key);
+
+	if (rc != 0) {
+		LOG_ERR("Error %d while deleting progress for \"%s\"",
+			rc, settings_key);
+	}
+
+	return rc;
+}
+
+#endif  /* CONFIG_STREAM_FLASH_PROGRESS */
diff --git a/tests/subsys/storage/stream/stream_flash/prj.conf b/tests/subsys/storage/stream/stream_flash/prj.conf
index 2b16be9..46211db 100644
--- a/tests/subsys/storage/stream/stream_flash/prj.conf
+++ b/tests/subsys/storage/stream/stream_flash/prj.conf
@@ -7,7 +7,11 @@
 CONFIG_ZTEST=y
 CONFIG_FLASH=y
 CONFIG_FLASH_PAGE_LAYOUT=y
+CONFIG_FLASH_MAP=y
+CONFIG_NVS=y
+CONFIG_SETTINGS=y
 CONFIG_DEBUG_OPTIMIZATIONS=y
 
 CONFIG_STREAM_FLASH=y
 CONFIG_STREAM_FLASH_ERASE=y
+CONFIG_STREAM_FLASH_PROGRESS=y
diff --git a/tests/subsys/storage/stream/stream_flash/src/main.c b/tests/subsys/storage/stream/stream_flash/src/main.c
index d00dcba..2f98a82 100644
--- a/tests/subsys/storage/stream/stream_flash/src/main.c
+++ b/tests/subsys/storage/stream/stream_flash/src/main.c
@@ -9,6 +9,7 @@
 #include <stdbool.h>
 #include <ztest.h>
 #include <drivers/flash.h>
+#include <settings/settings.h>
 
 #include <storage/stream_flash.h>
 
@@ -35,6 +36,8 @@
 static size_t cb_offset;
 static int cb_ret;
 
+static const char progress_key[] = "sf-test/progress";
+
 static uint8_t buf[BUF_LEN];
 static uint8_t read_buf[TESTBUF_SIZE];
 const static uint8_t write_buf[TESTBUF_SIZE] = {[0 ... TESTBUF_SIZE - 1] = 0xaa};
@@ -456,6 +459,186 @@
 }
 #endif
 
+static size_t write_and_save_progress(size_t bytes, const char *save_key)
+{
+	int rc;
+	size_t bytes_written;
+
+	rc = stream_flash_buffered_write(&ctx, write_buf, bytes, true);
+	zassert_equal(rc, 0, "expected success");
+
+	bytes_written = stream_flash_bytes_written(&ctx);
+	zassert_true(bytes_written > 0, "expected bytes to be written");
+
+	if (save_key) {
+		rc = stream_flash_progress_save(&ctx, save_key);
+		zassert_equal(rc, 0, "expected success");
+	}
+
+	return bytes_written;
+}
+
+static void clear_all_progress(void)
+{
+	(void) settings_delete(progress_key);
+}
+
+static size_t load_progress(const char *load_key)
+{
+	int rc;
+
+	rc = stream_flash_progress_load(&ctx, progress_key);
+	zassert_equal(rc, 0, "expected success");
+
+	return stream_flash_bytes_written(&ctx);
+}
+
+static void test_stream_flash_progress_api(void)
+{
+	int rc;
+
+	clear_all_progress();
+	init_target();
+
+	/* Test save parameter validation */
+	rc = stream_flash_progress_save(NULL, progress_key);
+	zassert_true(rc < 0, "expected error since ctx is NULL");
+
+	rc = stream_flash_progress_save(&ctx, NULL);
+	zassert_true(rc < 0, "expected error since key is NULL");
+
+	rc = stream_flash_progress_save(&ctx, progress_key);
+	zassert_equal(rc, 0, "expected success");
+
+	(void) write_and_save_progress(BUF_LEN, progress_key);
+
+	/* Test load parameter validation */
+	rc = stream_flash_progress_load(NULL, progress_key);
+	zassert_true(rc < 0, "expected error since ctx is NULL");
+
+	rc = stream_flash_progress_load(&ctx, NULL);
+	zassert_true(rc < 0, "expected error since key is NULL");
+
+	rc = stream_flash_progress_load(&ctx, progress_key);
+	zassert_equal(rc, 0, "expected success");
+
+	/* Test clear parameter validation */
+	rc = stream_flash_progress_clear(NULL, progress_key);
+	zassert_true(rc < 0, "expected error since ctx is NULL");
+
+	rc = stream_flash_progress_clear(&ctx, NULL);
+	zassert_true(rc < 0, "expected error since key is NULL");
+
+	rc = stream_flash_progress_clear(&ctx, progress_key);
+	zassert_equal(rc, 0, "expected success");
+}
+
+static void test_stream_flash_progress_resume(void)
+{
+	int rc;
+	size_t bytes_written_old;
+	size_t bytes_written;
+#ifdef CONFIG_STREAM_FLASH_ERASE
+	off_t erase_offset_old;
+	off_t erase_offset;
+#endif
+
+	clear_all_progress();
+	init_target();
+
+	bytes_written_old = stream_flash_bytes_written(&ctx);
+#ifdef CONFIG_STREAM_FLASH_ERASE
+	erase_offset_old = ctx.last_erased_page_start_offset;
+#endif
+
+	/* Test load with zero bytes_written */
+	rc = stream_flash_progress_save(&ctx, progress_key);
+	zassert_equal(rc, 0, "expected success");
+
+	rc = stream_flash_progress_load(&ctx, progress_key);
+	zassert_equal(rc, 0, "expected success");
+
+	bytes_written = stream_flash_bytes_written(&ctx);
+	zassert_equal(bytes_written, bytes_written_old,
+		      "expected bytes_written to be unchanged");
+#ifdef CONFIG_STREAM_FLASH_ERASE
+	erase_offset = ctx.last_erased_page_start_offset;
+	zassert_equal(erase_offset, erase_offset_old,
+		      "expected erase offset to be unchanged");
+#endif
+
+	clear_all_progress();
+	init_target();
+
+	/* Write some data and save the progress */
+	bytes_written_old = write_and_save_progress(page_size * 2,
+						    progress_key);
+#ifdef CONFIG_STREAM_FLASH_ERASE
+	erase_offset_old = ctx.last_erased_page_start_offset;
+	zassert_true(erase_offset_old != 0, "expected pages to be erased");
+#endif
+
+	init_target();
+
+	/* Load the previous progress */
+	bytes_written = load_progress(progress_key);
+	zassert_equal(bytes_written, bytes_written_old,
+		      "expected bytes_written to be loaded");
+#ifdef CONFIG_STREAM_FLASH_ERASE
+	zassert_equal(erase_offset_old, ctx.last_erased_page_start_offset,
+		      "expected last erased page offset to be loaded");
+#endif
+
+	/* Check that outdated progress does not overwrite current progress */
+	init_target();
+
+	(void) write_and_save_progress(BUF_LEN, progress_key);
+	bytes_written_old = write_and_save_progress(BUF_LEN, NULL);
+	bytes_written = load_progress(progress_key);
+	zassert_equal(bytes_written, bytes_written_old,
+		      "expected bytes_written to not be overwritten");
+}
+
+static void test_stream_flash_progress_clear(void)
+{
+	int rc;
+	size_t bytes_written_old;
+	size_t bytes_written;
+#ifdef CONFIG_STREAM_FLASH_ERASE
+	off_t erase_offset_old;
+	off_t erase_offset;
+#endif
+
+	clear_all_progress();
+	init_target();
+
+	/* Test that progress is cleared. */
+	(void) write_and_save_progress(BUF_LEN, progress_key);
+
+	rc = stream_flash_progress_clear(&ctx, progress_key);
+	zassert_equal(rc, 0, "expected success");
+
+	init_target();
+
+	bytes_written_old = stream_flash_bytes_written(&ctx);
+#ifdef CONFIG_STREAM_FLASH_ERASE
+	erase_offset_old = ctx.last_erased_page_start_offset;
+#endif
+
+	rc = stream_flash_progress_load(&ctx, progress_key);
+	zassert_equal(rc, 0, "expected success");
+
+	bytes_written = stream_flash_bytes_written(&ctx);
+	zassert_equal(bytes_written, bytes_written_old,
+		      "expected bytes_written to be unchanged");
+
+#ifdef CONFIG_STREAM_FLASH_ERASE
+	erase_offset = ctx.last_erased_page_start_offset;
+	zassert_equal(erase_offset, erase_offset_old,
+		      "expected erase offset to be unchanged");
+#endif
+}
+
 void test_main(void)
 {
 	fdev = device_get_binding(FLASH_NAME);
@@ -476,7 +659,10 @@
 	     ztest_unit_test(test_stream_flash_flush),
 	     ztest_unit_test(test_stream_flash_buffered_write_whole_page),
 	     ztest_unit_test(test_stream_flash_erase_page),
-	     ztest_unit_test(test_stream_flash_bytes_written)
+	     ztest_unit_test(test_stream_flash_bytes_written),
+	     ztest_unit_test(test_stream_flash_progress_api),
+	     ztest_unit_test(test_stream_flash_progress_resume),
+	     ztest_unit_test(test_stream_flash_progress_clear)
 	 );
 
 	ztest_run_test_suite(lib_stream_flash_test);