s3:pylibsmb: Add .savefile() API to SMB py bindings
authorTim Beale <timbeale@catalyst.net.nz>
Mon, 10 Dec 2018 20:34:42 +0000 (09:34 +1300)
committerTim Beale <timbeale@samba.org>
Mon, 7 Jan 2019 00:23:08 +0000 (01:23 +0100)
This provides a simple API for writing a file's contents and makes the
s3 API consistent with the s4 API.

All the async APIs here support SMBv2 so we don't need to use the sync
APIs at all.

Note that we have the choice here of using either cli_write_send() or
cli_push_send(). I chose the latter, because that's what smbclient uses.
It also appears to handle writing a large file better (i.e. one that
exceeds the max write size of the underlying connection).

BUG: https://bugzilla.samba.org/show_bug.cgi?id=13676

Signed-off-by: Tim Beale <timbeale@catalyst.net.nz>
Reviewed-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
python/samba/tests/smb.py
selftest/knownfail.d/smb [deleted file]
source3/libsmb/pylibsmb.c

index 28560ca71c978aef8c48b33c920ce5c3b96eb4ea..df590ffc5f861d60518686b19f9b28cc55368553 100644 (file)
@@ -113,7 +113,7 @@ class SMBTests(samba.tests.TestCase):
             for i in range(1, 4):
                 contents = "I'm file {0} in dir {1}!".format(i, subdir)
                 path = self.make_sysvol_path(subdir, "file-{0}.txt".format(i))
-                self.conn.savefile(path, test_contents.encode('utf8'))
+                self.smb_conn.savefile(path, test_contents.encode('utf8'))
                 filepaths.append(path)
 
         # sanity-check these dirs/files exist
@@ -166,7 +166,7 @@ class SMBTests(samba.tests.TestCase):
         """
         # create the test file
         self.assertFalse(self.file_exists(test_file))
-        self.conn.savefile(test_file, binary_contents)
+        self.smb_conn.savefile(test_file, binary_contents)
         self.assertTrue(self.file_exists(test_file))
 
         # delete it and check that it's gone
@@ -183,7 +183,7 @@ class SMBTests(samba.tests.TestCase):
         self.assertFalse(self.smb_conn.chkpath(bad_dir))
 
         # should return False for files (because they're not directories)
-        self.conn.savefile(test_file, binary_contents)
+        self.smb_conn.savefile(test_file, binary_contents)
         self.assertFalse(self.smb_conn.chkpath(test_file))
 
         # check correct result after creating and then deleting a new dir
@@ -195,7 +195,7 @@ class SMBTests(samba.tests.TestCase):
 
     def test_save_load_text(self):
 
-        self.conn.savefile(test_file, test_contents.encode('utf8'))
+        self.smb_conn.savefile(test_file, test_contents.encode('utf8'))
 
         contents = self.conn.loadfile(test_file)
         self.assertEquals(contents.decode('utf8'), test_contents,
@@ -203,7 +203,7 @@ class SMBTests(samba.tests.TestCase):
 
         # check we can overwrite the file with new contents
         new_contents = 'wxyz' * 128
-        self.conn.savefile(test_file, new_contents.encode('utf8'))
+        self.smb_conn.savefile(test_file, new_contents.encode('utf8'))
         contents = self.conn.loadfile(test_file)
         self.assertEquals(contents.decode('utf8'), new_contents,
                           msg='contents of test file did not match what was written')
@@ -211,7 +211,7 @@ class SMBTests(samba.tests.TestCase):
     # with python2 this will save/load str type (with embedded nulls)
     # with python3 this will save/load bytes type
     def test_save_load_string_bytes(self):
-        self.conn.savefile(test_file, test_literal_bytes_embed_nulls)
+        self.smb_conn.savefile(test_file, test_literal_bytes_embed_nulls)
 
         contents = self.conn.loadfile(test_file)
         self.assertEquals(contents, test_literal_bytes_embed_nulls,
@@ -220,7 +220,7 @@ class SMBTests(samba.tests.TestCase):
     # python3 only this will save/load unicode
     def test_save_load_utfcontents(self):
         if PY3:
-            self.conn.savefile(test_file, utf_contents.encode('utf8'))
+            self.smb_conn.savefile(test_file, utf_contents.encode('utf8'))
 
             contents = self.conn.loadfile(test_file)
             self.assertEquals(contents.decode('utf8'), utf_contents,
@@ -229,7 +229,7 @@ class SMBTests(samba.tests.TestCase):
     # with python2 this will save/load str type
     # with python3 this will save/load bytes type
     def test_save_binary_contents(self):
-        self.conn.savefile(test_file, binary_contents)
+        self.smb_conn.savefile(test_file, binary_contents)
 
         contents = self.conn.loadfile(test_file)
         self.assertEquals(contents, binary_contents,
diff --git a/selftest/knownfail.d/smb b/selftest/knownfail.d/smb
deleted file mode 100644 (file)
index 3c6324a..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-# currently savefile appends rather than overwriting
-samba.tests.smb.*samba.tests.smb.SMBTests.test_save_load_text\(ad_dc:local\)
index 156352d7b3df0810992559d6d8bf19b387dfc260..2a1ae29203545e64625163ee616d20a1bbeb7874 100644 (file)
@@ -741,6 +741,92 @@ static PyObject *py_cli_close(struct py_cli_state *self, PyObject *args)
        Py_RETURN_NONE;
 }
 
+struct push_state {
+       char *data;
+       off_t nread;
+       off_t total_data;
+};
+
+/*
+ * cli_push() helper to write a chunk of data to a remote file
+ */
+static size_t push_data(uint8_t *buf, size_t n, void *priv)
+{
+       struct push_state *state = (struct push_state *)priv;
+       char *curr_ptr = NULL;
+       off_t remaining;
+       size_t copied_bytes;
+
+       if (state->nread >= state->total_data) {
+               return 0;
+       }
+
+       curr_ptr = state->data + state->nread;
+       remaining = state->total_data - state->nread;
+       copied_bytes = MIN(remaining, n);
+
+       memcpy(buf, curr_ptr, copied_bytes);
+       state->nread += copied_bytes;
+       return copied_bytes;
+}
+
+/*
+ * Writes a file with the contents specified
+ */
+static PyObject *py_smb_savefile(struct py_cli_state *self, PyObject *args,
+                                PyObject *kwargs)
+{
+       uint16_t fnum;
+       const char *filename = NULL;
+       char *data = NULL;
+       Py_ssize_t size = 0;
+       NTSTATUS status;
+       struct tevent_req *req = NULL;
+       struct push_state state;
+
+       if (!PyArg_ParseTuple(args, "s"PYARG_BYTES_LEN":savefile", &filename,
+                             &data, &size)) {
+               return NULL;
+       }
+
+       /* create a new file handle for writing to */
+       req = cli_ntcreate_send(NULL, self->ev, self->cli, filename, 0,
+                               FILE_WRITE_DATA, FILE_ATTRIBUTE_NORMAL,
+                               FILE_SHARE_READ|FILE_SHARE_WRITE,
+                               FILE_OVERWRITE_IF, FILE_NON_DIRECTORY_FILE,
+                               SMB2_IMPERSONATION_IMPERSONATION, 0);
+       if (!py_tevent_req_wait_exc(self, req)) {
+               return NULL;
+       }
+       status = cli_ntcreate_recv(req, &fnum, NULL);
+       TALLOC_FREE(req);
+       PyErr_NTSTATUS_IS_ERR_RAISE(status);
+
+       /* write the new file contents */
+       state.data = data;
+       state.nread = 0;
+       state.total_data = size;
+
+       req = cli_push_send(NULL, self->ev, self->cli, fnum, 0, 0, 0,
+                           push_data, &state);
+       if (!py_tevent_req_wait_exc(self, req)) {
+               return NULL;
+       }
+       status = cli_push_recv(req);
+       TALLOC_FREE(req);
+       PyErr_NTSTATUS_IS_ERR_RAISE(status);
+
+       /* close the file handle */
+       req = cli_close_send(NULL, self->ev, self->cli, fnum);
+       if (!py_tevent_req_wait_exc(self, req)) {
+               return NULL;
+       }
+       status = cli_close_recv(req);
+       PyErr_NTSTATUS_IS_ERR_RAISE(status);
+
+       Py_RETURN_NONE;
+}
+
 static PyObject *py_cli_write(struct py_cli_state *self, PyObject *args,
                              PyObject *kwds)
 {
@@ -1214,6 +1300,9 @@ static PyMethodDef py_cli_state_methods[] = {
        { "chkpath", (PyCFunction)py_smb_chkpath, METH_VARARGS,
          "chkpath(dir_path) -> True or False\n\n"
          "\t\tReturn true if directory exists, false otherwise." },
+       { "savefile", (PyCFunction)py_smb_savefile, METH_VARARGS,
+         "savefile(path, str) -> None\n\n"
+         "\t\tWrite " PY_DESC_PY3_BYTES " str to file." },
        { NULL, NULL, 0, NULL }
 };