diff --git a/include/ws_functions/pwg.images.php b/include/ws_functions/pwg.images.php
index 09f3bbf0d..ef7286b9f 100644
--- a/include/ws_functions/pwg.images.php
+++ b/include/ws_functions/pwg.images.php
@@ -1446,6 +1446,294 @@ SELECT
}
}
+/**
+ * API method
+ * Adds a chunk of an image. Chunks don't have to be uploaded in the right sort order. When the last chunk is added, they get merged.
+ * @since 11
+ * @param mixed[] $params
+ * @option string username
+ * @option string password
+ * @option chunk int number of the chunk
+ * @option string chunk_sum MD5 sum of the chunk
+ * @option chunks int total number of chunks for this image
+ * @option string original_sum MD5 sum of the final image
+ * @option int[] category
+ * @option string filename
+ * @option string name (optional)
+ * @option string author (optional)
+ * @option string comment (optional)
+ * @option string date_creation (optional)
+ * @option int level
+ * @option string tag_ids (optional) - "tag_id,tag_id"
+ * @option int image_id (optional)
+ */
+function ws_images_uploadAsync($params, &$service)
+{
+ global $conf, $user, $logger;
+
+ // additional check for some parameters
+ if (!preg_match('/^[a-fA-F0-9]{32}$/', $params['original_sum']))
+ {
+ return new PwgError(WS_ERR_INVALID_PARAM, 'Invalid original_sum');
+ }
+
+ if (!try_log_user($params['username'], $params['password'], false))
+ {
+ return new PwgError(999, 'Invalid username/password');
+ }
+
+ // build $user
+ // include(PHPWG_ROOT_PATH.'include/user.inc.php');
+ $user = build_user($user['id'], false);
+
+ if (!is_admin())
+ {
+ return new PwgError(401, 'Admin status is required.');
+ }
+
+ if ($params['image_id'] > 0)
+ {
+ $query='
+SELECT COUNT(*)
+ FROM '. IMAGES_TABLE .'
+ WHERE id = '. $params['image_id'] .'
+;';
+ list($count) = pwg_db_fetch_row(pwg_query($query));
+ if ($count == 0)
+ {
+ return new PwgError(404, __FUNCTION__.' : image_id not found');
+ }
+ }
+
+ // handle upload error as in ws_images_addSimple
+ // if (isset($_FILES['image']['error']) && $_FILES['image']['error'] != 0)
+
+ $output_filepath_prefix = $conf['upload_dir'].'/buffer/'.$params['original_sum'].'-u'.$user['id'];
+ $chunkfile_path_pattern = $output_filepath_prefix.'-%03uof%03u.chunk';
+
+ $chunkfile_path = sprintf($chunkfile_path_pattern, $params['chunk']+1, $params['chunks']);
+
+ // create the upload directory tree if not exists
+ if (!mkgetdir(dirname($chunkfile_path), MKGETDIR_DEFAULT&~MKGETDIR_DIE_ON_ERROR))
+ {
+ return new PwgError(500, 'error during buffer directory creation');
+ }
+ secure_directory(dirname($chunkfile_path));
+
+ // move uploaded file
+ move_uploaded_file($_FILES['file']['tmp_name'], $chunkfile_path);
+ $logger->debug(__FUNCTION__.' uploaded '.$chunkfile_path);
+
+ // MD5 checksum
+ $chunk_md5 = md5_file($chunkfile_path);
+ if ($chunk_md5 != $params['chunk_sum'])
+ {
+ unlink($chunkfile_path);
+ $logger->error(__FUNCTION__.' '.$chunkfile_path.' MD5 checksum mismatched');
+ return new PwgError(500, "MD5 checksum chunk file mismatched");
+ }
+
+ // are all chunks uploaded?
+ $chunk_ids_uploaded = array();
+ for ($i = 1; $i <= $params['chunks']; $i++)
+ {
+ $chunkfile = sprintf($chunkfile_path_pattern, $i, $params['chunks']);
+ if ( file_exists($chunkfile) && ($fp = fopen($chunkfile, "rb"))!==false )
+ {
+ $chunk_ids_uploaded[] = $i;
+ fclose($fp);
+ }
+ }
+
+ if ($params['chunks'] != count($chunk_ids_uploaded))
+ {
+ // all chunks are not yet available
+ $logger->debug(__FUNCTION__.' all chunks are not uploaded yet, maybe on next chunk, exit for now');
+ return array('message' => 'chunks uploaded = '.implode(',', $chunk_ids_uploaded));
+ }
+
+ // all chunks available
+ $logger->debug(__FUNCTION__.' '.$params['original_sum'].' '.$params['chunks'].' chunks available, try now to get lock for merging');
+ $output_filepath = $output_filepath_prefix.'.merged';
+
+ // chunks already being merged?
+ if ( file_exists($output_filepath) && ($fp = fopen($output_filepath, "rb"))!==false )
+ {
+ // merge file already exists
+ fclose($fp);
+ $logger->error(__FUNCTION__.' '.$output_filepath.' already exists, another merge is under process');
+ return array('message' => 'chunks uploaded = '.implode(',', $chunk_ids_uploaded));
+ }
+
+ // create merged and open it for writing only
+ $fp = fopen($output_filepath, "wb");
+ if ( !$fp )
+ {
+ // unable to create file and open it for writing only
+ $logger->error(__FUNCTION__.' '.$chunkfile_path.' unable to create merge file');
+ return new PwgError(500, 'error while creating merged '.$chunkfile_path);
+ }
+
+ // acquire an exclusive lock and keep it until merge completes
+ // this postpones another uploadAsync task running in another thread
+ if (!flock($fp, LOCK_EX))
+ {
+ // unable to obtain lock
+ fclose($fp);
+ $logger->error(__FUNCTION__.' '.$chunkfile_path.' unable to obtain lock');
+ return new PwgError(500, 'error while locking merged '.$chunkfile_path);
+ }
+
+ $logger->debug(__FUNCTION__.' lock obtained to merge chunks');
+
+ // loop over all chunks
+ foreach ($chunk_ids_uploaded as $chunk_id)
+ {
+ $chunkfile_path = sprintf($chunkfile_path_pattern, $chunk_id, $params['chunks']);
+
+ // chunk deleted by preceding merge?
+ if (!file_exists($chunkfile_path))
+ {
+ // cancel merge
+ $logger->error(__FUNCTION__.' '.$chunkfile_path.' already merged');
+ flock($fp, LOCK_UN);
+ fclose($fp);
+ return array('message' => 'chunks uploaded = '.implode(',', $chunk_ids_uploaded));
+ }
+
+ if (!fwrite($fp, file_get_contents($chunkfile_path)))
+ {
+ // could not append chunk
+ $logger->error(__FUNCTION__.' error merging chunk '.$chunkfile_path);
+ flock($fp, LOCK_UN);
+ fclose($fp);
+
+ // delete merge file without returning an error
+ @unlink($output_filepath);
+ return new PwgError(500, 'error while merging chunk '.$chunk_id);
+ }
+
+ $logger->debug(__FUNCTION__.' original_sum='.$params['original_sum'].', chunk '.$chunk_id.'/'.$params['chunks'].' merged');
+
+ // delete chunk and clear cache
+ unlink($chunkfile_path);
+ }
+
+ // flush output before releasing lock
+ fflush($fp);
+ flock($fp, LOCK_UN);
+ fclose($fp);
+
+ $logger->debug(__FUNCTION__.' merged file '.$output_filepath.' saved');
+
+ // MD5 checksum
+ $merged_md5 = md5_file($output_filepath);
+
+ if ($merged_md5 != $params['original_sum'])
+ {
+ unlink($output_filepath);
+ $logger->error(__FUNCTION__.' '.$output_filepath.' MD5 checksum mismatched!');
+ return new PwgError(500, "MD5 checksum merged file mismatched");
+ }
+
+ $logger->debug(__FUNCTION__.' '.$output_filepath.' MD5 checksum OK');
+
+ include_once(PHPWG_ROOT_PATH.'admin/include/functions_upload.inc.php');
+
+ $image_id = add_uploaded_file(
+ $output_filepath,
+ $params['filename'],
+ $params['category'],
+ $params['level'],
+ $params['image_id'],
+ $params['original_sum']
+ );
+
+ $logger->debug(__FUNCTION__.' image_id after add_uploaded_file = '.$image_id);
+
+ // and now, let's create tag associations
+ if (isset($params['tag_ids']) and !empty($params['tag_ids']))
+ {
+ set_tags(
+ explode(',', $params['tag_ids']),
+ $image_id
+ );
+ }
+
+ // time to set other infos
+ $info_columns = array(
+ 'name',
+ 'author',
+ 'comment',
+ 'date_creation',
+ );
+
+ $update = array();
+ foreach ($info_columns as $key)
+ {
+ if (isset($params[$key]))
+ {
+ $update[$key] = $params[$key];
+ }
+ }
+
+ if (count(array_keys($update)) > 0)
+ {
+ single_update(
+ IMAGES_TABLE,
+ $update,
+ array('id' => $image_id)
+ );
+ }
+
+ // final step, reset user cache
+ invalidate_user_cache();
+
+ // trick to bypass get_sql_condition_FandF
+ if (!empty($params['level']) and $params['level'] > $user['level'])
+ {
+ // this will not persist
+ $user['level'] = $params['level'];
+ }
+
+ // delete chunks older than a week
+ $now = time();
+ foreach (glob($conf['upload_dir'].'/buffer/'."*.chunk") as $file)
+ {
+ if (is_file($file))
+ {
+ if ($now - filemtime($file) >= 60 * 60 * 24 * 7) // 7 days
+ {
+ $logger->info(__FUNCTION__.' delete '.$file);
+ unlink($file);
+ }
+ else
+ {
+ $logger->debug(__FUNCTION__.' keep '.$file);
+ }
+ }
+ }
+
+ // delete merged older than a week
+ foreach (glob($conf['upload_dir'].'/buffer/'."*.merged") as $file)
+ {
+ if (is_file($file))
+ {
+ if ($now - filemtime($file) >= 60 * 60 * 24 * 7) // 7 days
+ {
+ $logger->info(__FUNCTION__.' delete '.$file);
+ unlink($file);
+ }
+ else
+ {
+ $logger->debug(__FUNCTION__.' keep '.$file);
+ }
+ }
+ }
+
+ return $service->invoke('pwg.images.getInfo', array('image_id' => $image_id));
+}
+
/**
* API method
* Check if an image exists by it's name or md5 sum
diff --git a/ws.php b/ws.php
index 627e0cd98..bceda3a04 100644
--- a/ws.php
+++ b/ws.php
@@ -485,6 +485,36 @@ function ws_addDefaultMethods( $arr )
$ws_functions_root . 'pwg.images.php',
array('admin_only'=>true, 'post_only'=>true)
);
+
+ $service->addMethod(
+ 'pwg.images.uploadAsync',
+ 'ws_images_uploadAsync',
+ array(
+ 'username' => array(),
+ 'password' => array('default'=>null),
+ 'chunk' => array('type'=>WS_TYPE_INT|WS_TYPE_POSITIVE),
+ 'chunk_sum' => array(),
+ 'chunks' => array('type'=>WS_TYPE_INT|WS_TYPE_POSITIVE),
+ 'original_sum' => array(),
+ 'category' => array('default'=>null, 'flags'=>WS_PARAM_FORCE_ARRAY, 'type'=>WS_TYPE_ID),
+ 'filename' => array(),
+ 'name' => array('default'=>null),
+ 'author' => array('default'=>null),
+ 'comment' => array('default'=>null),
+ 'date_creation' => array('default'=>null),
+ 'level' => array('default'=>0, 'maxValue'=>max($conf['available_permission_levels']), 'type'=>WS_TYPE_INT|WS_TYPE_POSITIVE),
+ 'tag_ids' => array('default'=>null, 'info'=>'Comma separated ids'),
+ 'image_id' => array('default'=>null, 'type'=>WS_TYPE_ID),
+ ),
+ 'Upload photo by chunks in a random order.
+
Use the $_FILES[file] field for uploading file.
+
Start with chunk 0 (zero).
+
Set the form encoding to "form-data".
+
You can update an existing photo if you define an existing image_id.
+
Requires admin credentials.',
+ $ws_functions_root . 'pwg.images.php',
+ array('post_only'=>true)
+ );
$service->addMethod(
'pwg.images.delete',