Merge pull request #47 from sismics/master

Push to production
This commit is contained in:
Benjamin Gamard 2015-11-30 00:10:26 +01:00
commit 8477920475
55 changed files with 1166 additions and 333 deletions

View File

@ -28,6 +28,7 @@ Features
- Tag system with relations
- Multi-users ACL system
- Audit log
- Comments
- Document sharing by URL
- RESTful Web API
- Fully featured Android client

View File

@ -71,12 +71,11 @@
<excludeFolder url="file://$MODULE_DIR$/build/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/debug" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/22.2.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/recyclerview-v7/22.2.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/22.2.1/jars" />
@ -84,17 +83,23 @@
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/it.sephiroth.android.library.easing/android-easing/1.0.3/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/it.sephiroth.android.library.imagezoom/imagezoom/1.0.5/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-runtime-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-verifier" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant-run-support" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/reload-dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/resources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/restart-dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/tmp" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
<excludeFolder url="file://$MODULE_DIR$/build/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/ndk" />
@ -112,12 +117,12 @@
<orderEntry type="library" exported="" name="android-easing-1.0.3" level="project" />
<orderEntry type="library" exported="" name="imagezoom-1.0.5" level="project" />
<orderEntry type="library" exported="" name="support-v4-22.2.1" level="project" />
<orderEntry type="library" exported="" name="eventbus-2.4.0" level="project" />
<orderEntry type="library" exported="" name="recyclerview-v7-22.2.1" level="project" />
<orderEntry type="library" exported="" name="android-query.0.26.8" level="project" />
<orderEntry type="library" exported="" name="tokenautocomplete-1.2.1" level="project" />
<orderEntry type="library" exported="" name="support-annotations-22.2.1" level="project" />
<orderEntry type="library" exported="" name="appcompat-v7-22.2.1" level="project" />
<orderEntry type="library" exported="" name="android-async-http-1.4.6" level="project" />
<orderEntry type="library" exported="" name="eventbus-2.4.1" level="project" />
</component>
</module>

View File

@ -1,20 +1,20 @@
buildscript {
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.0'
classpath 'com.android.tools.build:gradle:2.0.0-alpha1'
}
}
apply plugin: 'com.android.application'
repositories {
mavenCentral()
jcenter()
}
android {
compileSdkVersion 22
buildToolsVersion "23.0.2"
buildToolsVersion '23.0.2'
defaultConfig {
minSdkVersion 14
@ -54,6 +54,6 @@ dependencies {
compile 'com.android.support:recyclerview-v7:22.+'
compile 'com.loopj.android:android-async-http:1.4.6'
compile 'it.sephiroth.android.library.imagezoom:imagezoom:1.0.5'
compile 'de.greenrobot:eventbus:2.4.0'
compile 'de.greenrobot:eventbus:2.4.1'
compile 'com.shamanland:fab:0.0.6'
}

View File

@ -20,7 +20,6 @@ public class MainApplication extends Application {
JSONObject json = PreferenceUtil.getCachedJson(getApplicationContext(), PreferenceUtil.PREF_CACHED_USER_INFO_JSON);
ApplicationContext.getInstance().setUserInfo(getApplicationContext(), json);
// TODO google docs app: right drawer with all actions, with acls, with deep metadatas
// TODO Provide documents to intent action get content
super.onCreate();
@ -28,6 +27,7 @@ public class MainApplication extends Application {
@Override
public void onLowMemory() {
super.onLowMemory();
BitmapAjaxCallback.clearCache();
}
}

View File

@ -19,11 +19,15 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
@ -31,7 +35,10 @@ import android.widget.Toast;
import com.sismics.docs.R;
import com.sismics.docs.adapter.AclListAdapter;
import com.sismics.docs.adapter.CommentListAdapter;
import com.sismics.docs.adapter.FilePagerAdapter;
import com.sismics.docs.event.CommentAddEvent;
import com.sismics.docs.event.CommentDeleteEvent;
import com.sismics.docs.event.DocumentDeleteEvent;
import com.sismics.docs.event.DocumentEditEvent;
import com.sismics.docs.event.DocumentFullscreenEvent;
@ -40,6 +47,7 @@ import com.sismics.docs.event.FileDeleteEvent;
import com.sismics.docs.fragment.DocShareFragment;
import com.sismics.docs.listener.JsonHttpResponseHandler;
import com.sismics.docs.model.application.ApplicationContext;
import com.sismics.docs.resource.CommentResource;
import com.sismics.docs.resource.DocumentResource;
import com.sismics.docs.resource.FileResource;
import com.sismics.docs.service.FileUploadService;
@ -83,6 +91,11 @@ public class DocumentViewActivity extends AppCompatActivity {
*/
private FilePagerAdapter filePagerAdapter;
/**
* Comment list adapter.
*/
private CommentListAdapter commentListAdapter;
/**
* Document displayed.
*/
@ -241,6 +254,39 @@ public class DocumentViewActivity extends AppCompatActivity {
}
});
// Button add a comment
ImageButton imageButton = (ImageButton) findViewById(R.id.addCommentBtn);
imageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
final EditText commentEditText = (EditText) findViewById(R.id.commentEditText);
if (commentEditText.getText().length() == 0) {
// No content for the new comment
return;
}
Toast.makeText(DocumentViewActivity.this, R.string.adding_comment, Toast.LENGTH_LONG).show();
CommentResource.add(DocumentViewActivity.this,
DocumentViewActivity.this.document.optString("id"),
commentEditText.getText().toString(),
new JsonHttpResponseHandler() {
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
EventBus.getDefault().post(new CommentAddEvent(response));
commentEditText.setText("");
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
Toast.makeText(DocumentViewActivity.this, R.string.comment_add_failure, Toast.LENGTH_LONG).show();
}
});
}
});
// Grab the comments
updateComments();
// Grab the attached files
updateFiles();
@ -268,6 +314,15 @@ public class DocumentViewActivity extends AppCompatActivity {
}
return true;
case R.id.comments:
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START);
} else {
drawerLayout.openDrawer(GravityCompat.START);
}
return true;
case R.id.download_file:
downloadCurrentFile();
return true;
@ -507,6 +562,36 @@ public class DocumentViewActivity extends AppCompatActivity {
}
}
/**
* A comment add event has been fired.
*
* @param event Comment add event
*/
public void onEventMainThread(CommentAddEvent event) {
if (commentListAdapter == null) return;
TextView emptyView = (TextView) findViewById(R.id.commentEmptyView);
ListView listView = (ListView) findViewById(R.id.commentListView);
emptyView.setVisibility(View.GONE);
listView.setVisibility(View.VISIBLE);
commentListAdapter.add(event.getComment());
}
/**
* A comment delete event has been fired.
*
* @param event Comment add event
*/
public void onEventMainThread(CommentDeleteEvent event) {
if (commentListAdapter == null) return;
TextView emptyView = (TextView) findViewById(R.id.commentEmptyView);
ListView listView = (ListView) findViewById(R.id.commentListView);
commentListAdapter.remove(event.getCommentId());
if (commentListAdapter.getCount() == 0) {
emptyView.setVisibility(View.VISIBLE);
listView.setVisibility(View.GONE);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (document == null) return;
@ -572,6 +657,89 @@ public class DocumentViewActivity extends AppCompatActivity {
});
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
switch (view.getId()) {
case R.id.commentListView:
if (commentListAdapter == null || document == null) return;
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
JSONObject comment = commentListAdapter.getItem(info.position);
boolean writable = document.optBoolean("writable");
String creator = comment.optString("creator");
String username = ApplicationContext.getInstance().getUserInfo().optString("username");
if (writable || creator.equals(username)) {
menu.add(Menu.NONE, 0, 0, getString(R.string.comment_delete));
}
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
// Use real ids if more than one item someday
if (item.getItemId() == 0) {
// Delete a comment
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
if (commentListAdapter == null) return false;
JSONObject comment = commentListAdapter.getItem(info.position);
final String commentId = comment.optString("id");
Toast.makeText(DocumentViewActivity.this, R.string.deleting_comment, Toast.LENGTH_LONG).show();
CommentResource.remove(DocumentViewActivity.this, commentId, new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
EventBus.getDefault().post(new CommentDeleteEvent(commentId));
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
Toast.makeText(DocumentViewActivity.this, R.string.error_deleting_comment, Toast.LENGTH_LONG).show();
}
});
return true;
}
return false;
}
/**
* Refresh comments list.
*/
private void updateComments() {
if (document == null) return;
final View progressBar = findViewById(R.id.commentProgressView);
final TextView emptyView = (TextView) findViewById(R.id.commentEmptyView);
final ListView listView = (ListView) findViewById(R.id.commentListView);
progressBar.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
listView.setVisibility(View.GONE);
registerForContextMenu(listView);
CommentResource.list(this, document.optString("id"), new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
JSONArray comments = response.optJSONArray("comments");
commentListAdapter = new CommentListAdapter(comments);
listView.setAdapter(commentListAdapter);
listView.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
if (comments.length() == 0) {
listView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
}
@Override
public void onAllFailure(int statusCode, Header[] headers, byte[] responseBytes, Throwable throwable) {
emptyView.setText(R.string.error_loading_comments);
progressBar.setVisibility(View.GONE);
listView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
});
}
/**
* Refresh files list.
*/

View File

@ -0,0 +1,127 @@
package com.sismics.docs.adapter;
import android.content.Context;
import android.graphics.Bitmap;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.androidquery.AQuery;
import com.androidquery.callback.BitmapAjaxCallback;
import com.sismics.docs.R;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Comment list adapter.
*
* @author bgamard.
*/
public class CommentListAdapter extends BaseAdapter {
/**
* AQuery.
*/
private AQuery aq;
/**
* Tags.
*/
private List<JSONObject> commentList = new ArrayList<>();
/**
* Comment list adapter.
*
* @param commentsArray Comments
*/
public CommentListAdapter(JSONArray commentsArray) {
for (int i = 0; i < commentsArray.length(); i++) {
commentList.add(commentsArray.optJSONObject(i));
}
}
@Override
public int getCount() {
return commentList.size();
}
@Override
public JSONObject getItem(int position) {
return commentList.get(position);
}
@Override
public long getItemId(int position) {
return getItem(position).optString("id").hashCode();
}
@Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
LayoutInflater vi = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.comment_list_item, parent, false);
}
if (aq == null) {
aq = new AQuery(view);
} else {
aq.recycle(view);
}
// Fill the view
JSONObject comment = getItem(position);
TextView creatorTextView = (TextView) view.findViewById(R.id.creatorTextView);
TextView dateTextView = (TextView) view.findViewById(R.id.dateTextView);
TextView contentTextView = (TextView) view.findViewById(R.id.contentTextView);
ImageView gravatarImageView = (ImageView) view.findViewById(R.id.gravatarImageView);
creatorTextView.setText(comment.optString("creator"));
dateTextView.setText(DateFormat.getDateFormat(dateTextView.getContext()).format(new Date(comment.optLong("create_date"))));
contentTextView.setText(comment.optString("content"));
// Gravatar image
String gravatarUrl = "http://www.gravatar.com/avatar/" + comment.optString("creator_gravatar") + "?s=128d=identicon";
if (aq.shouldDelay(position, view, parent, gravatarUrl)) {
aq.id(gravatarImageView).image((Bitmap) null);
} else {
aq.id(gravatarImageView).image(new BitmapAjaxCallback()
.url(gravatarUrl)
.animation(AQuery.FADE_IN_NETWORK)
);
}
return view;
}
/**
* Add a new comment.
*
* @param comment Comment
*/
public void add(JSONObject comment) {
commentList.add(comment);
notifyDataSetChanged();
}
/**
* Remove a comment.
*
* @param commentId Comment ID
*/
public void remove(String commentId) {
for (JSONObject comment : commentList) {
if (comment.optString("id").equals(commentId)) {
commentList.remove(comment);
notifyDataSetChanged();
return;
}
}
}
}

View File

@ -0,0 +1,33 @@
package com.sismics.docs.event;
import org.json.JSONObject;
/**
* Comment add event.
*
* @author bgamard.
*/
public class CommentAddEvent {
/**
* Comment.
*/
private JSONObject comment;
/**
* Create a comment add event.
*
* @param comment Comment
*/
public CommentAddEvent(JSONObject comment) {
this.comment = comment;
}
/**
* Getter of comment.
*
* @return comment
*/
public JSONObject getComment() {
return comment;
}
}

View File

@ -0,0 +1,31 @@
package com.sismics.docs.event;
/**
* Comment delete event.
*
* @author bgamard.
*/
public class CommentDeleteEvent {
/**
* Comment ID.
*/
private String commentId;
/**
* Create a comment add event.
*
* @param commentId Comment ID
*/
public CommentDeleteEvent(String commentId) {
this.commentId = commentId;
}
/**
* Getter of commentId.
*
* @return commentId
*/
public String getCommentId() {
return commentId;
}
}

View File

@ -0,0 +1,66 @@
package com.sismics.docs.resource;
import android.content.Context;
import com.loopj.android.http.RequestParams;
import com.sismics.docs.listener.JsonHttpResponseHandler;
/**
* Access to /comment API.
*
* @author bgamard
*/
public class CommentResource extends BaseResource {
/**
* GET /comment/id.
*
* @param context Context
* @param documentId Document ID
* @param responseHandler Callback
*/
public static void list(Context context, String documentId, JsonHttpResponseHandler responseHandler) {
init(context);
client.get(getApiUrl(context) + "/comment/" + documentId, responseHandler);
}
/**
* PUT /comment.
*
* @param context Context
* @param documentId Document ID
* @param content Comment content
* @param responseHandler Callback
*/
public static void add(Context context, String documentId, String content, JsonHttpResponseHandler responseHandler) {
init(context);
RequestParams params = new RequestParams();
params.put("id", documentId);
params.put("content", content);
client.put(getApiUrl(context) + "/comment", params, responseHandler);
}
/**
* DELETE /comment/id.
*
* @param context Context
* @param commentId Comment ID
* @param responseHandler Callback
*/
public static void remove(Context context, String commentId, JsonHttpResponseHandler responseHandler) {
init(context);
client.delete(getApiUrl(context) + "/comment/" + commentId, responseHandler);
}
/**
* Cancel pending requests.
*
* @param context Context
*/
public static void cancel(Context context) {
client.cancelRequests(context, true);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/gravatarImageView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginRight="12dp"/>
<TextView
android:id="@+id/creatorTextView"
android:layout_toRightOf="@id/gravatarImageView"
android:layout_toEndOf="@id/gravatarImageView"
android:layout_alignParentTop="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:textStyle="bold"
android:textColor="#212121"
android:text="Creator"
android:textSize="14sp"/>
<TextView
android:id="@+id/contentTextView"
android:layout_toRightOf="@id/gravatarImageView"
android:layout_toEndOf="@id/gravatarImageView"
android:layout_below="@id/creatorTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:fontFamily="sans-serif"
android:textColor="#212121"
android:text="Comment content"
android:textSize="14sp"/>
<TextView
android:id="@+id/dateTextView"
android:layout_toRightOf="@id/gravatarImageView"
android:layout_toEndOf="@id/gravatarImageView"
android:layout_below="@id/contentTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:fontFamily="sans-serif"
android:textColor="#888"
android:text="2015-11-10"
android:textSize="14sp"/>
</RelativeLayout>

View File

@ -37,6 +37,109 @@
</RelativeLayout>
<!-- Left drawer -->
<LinearLayout
android:id="@+id/left_drawer"
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:orientation="vertical"
android:clickable="true"
android:background="#fff"
android:elevation="5dp">
<!-- Comments -->
<TextView
android:drawableStart="@drawable/ic_comment_black_24dp"
android:drawableLeft="@drawable/ic_comment_black_24dp"
android:drawablePadding="6dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:gravity="center"
android:textColor="@color/primary_text_default_material_light"
android:text="@string/comments"
android:layout_margin="12dp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#eee"/>
<ListView
android:layout_weight="1"
android:id="@+id/commentListView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:transcriptMode="normal"
android:dividerHeight="0dp"/>
<RelativeLayout
android:layout_weight="1"
android:id="@+id/commentProgressView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone">
<ProgressBar
style="?android:progressBarStyle"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</RelativeLayout>
<TextView
android:id="@+id/commentEmptyView"
android:visibility="gone"
android:padding="12dp"
android:gravity="center"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fontFamily="sans-serif-light"
android:text="@string/no_comments"
android:textSize="14sp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#eee"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="6dp"
android:gravity="center">
<EditText
android:id="@+id/commentEditText"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:lines="1"
android:inputType="text"
android:hint="@string/add_comment"
android:maxLength="4000"/>
<ImageButton
android:id="@+id/addCommentBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_send_grey600_24dp"
android:contentDescription="@string/send"
android:background="?android:selectableItemBackground"/>
</LinearLayout>
</LinearLayout>
<!-- Right drawer -->
<LinearLayout

View File

@ -9,6 +9,12 @@
android:title="@string/toggle_informations">
</item>
<item
android:id="@+id/comments"
app:showAsAction="collapseActionView"
android:title="@string/comments">
</item>
<item
android:id="@+id/download_file"
app:showAsAction="collapseActionView"

View File

@ -107,6 +107,16 @@
<string name="all_languages">All languages</string>
<string name="toggle_informations">Toggle informations</string>
<string name="who_can_access">Who can access</string>
<string name="comments">Comments</string>
<string name="no_comments">No comments</string>
<string name="error_loading_comments">Error loading comments</string>
<string name="send">Send</string>
<string name="add_comment">Add a comment</string>
<string name="comment_add_failure">Error adding a comment</string>
<string name="adding_comment">Adding a comment</string>
<string name="comment_delete">Delete comment</string>
<string name="deleting_comment">Deleting comment</string>
<string name="error_deleting_comment">Error deleting comment</string>
</resources>

View File

@ -1,6 +1,6 @@
#Wed Nov 26 21:58:48 CET 2014
#Mon Nov 23 20:12:30 CET 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip

View File

@ -0,0 +1 @@
update T_CONFIG set CFG_VALUE_C = 'RAM' where CFG_ID_C = 'LUCENE_DIRECTORY_STORAGE';

View File

@ -101,6 +101,8 @@ public class UserDao {
// Update the user
userFromDb.setEmail(user.getEmail());
userFromDb.setStorageQuota(user.getStorageQuota());
userFromDb.setStorageCurrent(user.getStorageCurrent());
// Create audit log
AuditLogUtil.create(userFromDb, AuditLogType.UPDATE);
@ -226,7 +228,7 @@ public class UserDao {
Map<String, Object> parameterMap = new HashMap<String, Object>();
List<String> criteriaList = new ArrayList<String>();
StringBuilder sb = new StringBuilder("select u.USE_ID_C as c0, u.USE_USERNAME_C as c1, u.USE_EMAIL_C as c2, u.USE_CREATEDATE_D as c3");
StringBuilder sb = new StringBuilder("select u.USE_ID_C as c0, u.USE_USERNAME_C as c1, u.USE_EMAIL_C as c2, u.USE_CREATEDATE_D as c3, u.USE_STORAGECURRENT_N as c4, u.USE_STORAGEQUOTA_N as c5");
sb.append(" from T_USER u ");
// Add search criterias
@ -255,6 +257,8 @@ public class UserDao {
userDto.setUsername((String) o[i++]);
userDto.setEmail((String) o[i++]);
userDto.setCreateTimestamp(((Timestamp) o[i++]).getTime());
userDto.setStorageCurrent(((Number) o[i++]).longValue());
userDto.setStorageQuota(((Number) o[i++]).longValue());
userDtoList.add(userDto);
}
paginatedList.setResultList(userDtoList);

View File

@ -1,5 +1,6 @@
package com.sismics.docs.core.dao.jpa.dto;
/**
* User DTO.
*
@ -26,6 +27,16 @@ public class UserDto {
*/
private Long createTimestamp;
/**
* Storage quota.
*/
private Long storageQuota;
/**
* Storage current usage.
*/
private Long storageCurrent;
/**
* Getter of id.
*
@ -88,6 +99,22 @@ public class UserDto {
public Long getCreateTimestamp() {
return createTimestamp;
}
public Long getStorageQuota() {
return storageQuota;
}
public void setStorageQuota(Long storageQuota) {
this.storageQuota = storageQuota;
}
public Long getStorageCurrent() {
return storageCurrent;
}
public void setStorageCurrent(Long storageCurrent) {
this.storageCurrent = storageCurrent;
}
/**
* Setter of createTimestamp.

View File

@ -54,6 +54,18 @@ public class User implements Loggable {
@Column(name = "USE_EMAIL_C", nullable = false, length = 100)
private String email;
/**
* Storage quota.
*/
@Column(name = "USE_STORAGEQUOTA_N", nullable = false)
private Long storageQuota;
/**
* Storage current usage.
*/
@Column(name = "USE_STORAGECURRENT_N", nullable = false)
private Long storageCurrent;
/**
* Creation date.
*/
@ -66,149 +78,87 @@ public class User implements Loggable {
@Column(name = "USE_DELETEDATE_D")
private Date deleteDate;
/**
* Getter of id.
*
* @return id
*/
public String getId() {
return id;
}
/**
* Setter of id.
*
* @param id id
*/
public void setId(String id) {
this.id = id;
}
/**
* Getter of roleId.
*
* @return roleId
*/
public String getRoleId() {
return roleId;
}
/**
* Setter of roleId.
*
* @param roleId roleId
*/
public void setRoleId(String roleId) {
this.roleId = roleId;
}
/**
* Getter of username.
*
* @return username
*/
public String getUsername() {
return username;
}
/**
* Setter of username.
*
* @param username username
*/
public void setUsername(String username) {
this.username = username;
}
/**
* Getter of password.
*
* @return password
*/
public String getPassword() {
return password;
}
/**
* Setter of password.
*
* @param password password
*/
public void setPassword(String password) {
this.password = password;
}
/**
* Getter of email.
*
* @return email
*/
public String getEmail() {
return email;
}
/**
* Setter of email.
*
* @param email email
*/
public void setEmail(String email) {
this.email = email;
}
/**
* Getter of createDate.
*
* @return createDate
*/
public Date getCreateDate() {
return createDate;
}
/**
* Setter of createDate.
*
* @param createDate createDate
*/
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
/**
* Getter of deleteDate.
*
* @return deleteDate
*/
@Override
public Date getDeleteDate() {
return deleteDate;
}
/**
* Setter of deleteDate.
*
* @param deleteDate deleteDate
*/
public void setDeleteDate(Date deleteDate) {
this.deleteDate = deleteDate;
}
/**
* Getter de privateKey.
* @return privateKey
*/
public String getPrivateKey() {
return privateKey;
}
/**
* Setter de privateKey.
* @param privateKey privateKey
*/
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public Long getStorageQuota() {
return storageQuota;
}
public void setStorageQuota(Long storageQuota) {
this.storageQuota = storageQuota;
}
public Long getStorageCurrent() {
return storageCurrent;
}
public void setStorageCurrent(Long storageCurrent) {
this.storageCurrent = storageCurrent;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)

View File

@ -1,7 +1,7 @@
package com.sismics.docs.core.service;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import org.apache.lucene.index.DirectoryReader;
@ -56,10 +56,10 @@ public class IndexingService extends AbstractScheduledService {
directory = new RAMDirectory();
log.info("Using RAM Lucene storage");
} else if (luceneStorageConfig.equals(Constants.LUCENE_DIRECTORY_STORAGE_FILE)) {
File luceneDirectory = DirectoryUtil.getLuceneDirectory();
Path luceneDirectory = DirectoryUtil.getLuceneDirectory();
log.info("Using file Lucene storage: {}", luceneDirectory);
try {
directory = new SimpleFSDirectory(luceneDirectory, new SimpleFSLockFactory());
directory = new SimpleFSDirectory(luceneDirectory.toFile(), new SimpleFSLockFactory());
} catch (IOException e) {
log.error("Error initializing Lucene index", e);
}

View File

@ -1,6 +1,9 @@
package com.sismics.docs.core.util;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.commons.lang.StringUtils;
@ -17,27 +20,31 @@ public class DirectoryUtil {
*
* @return Base data directory
*/
public static File getBaseDataDirectory() {
File baseDataDir = null;
public static Path getBaseDataDirectory() {
Path baseDataDir = null;
if (StringUtils.isNotBlank(EnvironmentUtil.getDocsHome())) {
// If the docs.home property is set then use it
baseDataDir = new File(EnvironmentUtil.getDocsHome());
baseDataDir = Paths.get(EnvironmentUtil.getDocsHome());
} else if (EnvironmentUtil.isUnitTest()) {
// For unit testing, use a temporary directory
baseDataDir = new File(System.getProperty("java.io.tmpdir"));
baseDataDir = Paths.get(System.getProperty("java.io.tmpdir"));
} else {
// We are in a webapp environment and nothing is specified, use the default directory for this OS
if (EnvironmentUtil.isUnix()) {
baseDataDir = new File("/var/docs");
baseDataDir = Paths.get("/var/docs");
} if (EnvironmentUtil.isWindows()) {
baseDataDir = new File(EnvironmentUtil.getWindowsAppData() + "\\Sismics\\Docs");
baseDataDir = Paths.get(EnvironmentUtil.getWindowsAppData() + "\\Sismics\\Docs");
} else if (EnvironmentUtil.isMacOs()) {
baseDataDir = new File(EnvironmentUtil.getMacOsUserHome() + "/Library/Sismics/Docs");
baseDataDir = Paths.get(EnvironmentUtil.getMacOsUserHome() + "/Library/Sismics/Docs");
}
}
if (baseDataDir != null && !baseDataDir.isDirectory()) {
baseDataDir.mkdirs();
if (baseDataDir != null && !Files.isDirectory(baseDataDir)) {
try {
Files.createDirectories(baseDataDir);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return baseDataDir;
@ -48,7 +55,7 @@ public class DirectoryUtil {
*
* @return Database directory.
*/
public static File getDbDirectory() {
public static Path getDbDirectory() {
return getDataSubDirectory("db");
}
@ -57,7 +64,7 @@ public class DirectoryUtil {
*
* @return Lucene indexes directory.
*/
public static File getLuceneDirectory() {
public static Path getLuceneDirectory() {
return getDataSubDirectory("lucene");
}
@ -66,7 +73,7 @@ public class DirectoryUtil {
*
* @return Storage directory.
*/
public static File getStorageDirectory() {
public static Path getStorageDirectory() {
return getDataSubDirectory("storage");
}
@ -75,7 +82,7 @@ public class DirectoryUtil {
*
* @return Log directory.
*/
public static File getLogDirectory() {
public static Path getLogDirectory() {
return getDataSubDirectory("log");
}
@ -84,11 +91,15 @@ public class DirectoryUtil {
*
* @return Subdirectory
*/
private static File getDataSubDirectory(String subdirectory) {
File baseDataDir = getBaseDataDirectory();
File directory = new File(baseDataDir.getPath() + File.separator + subdirectory);
if (!directory.isDirectory()) {
directory.mkdirs();
private static Path getDataSubDirectory(String subdirectory) {
Path baseDataDir = getBaseDataDirectory();
Path directory = baseDataDir.resolve(subdirectory);
if (!Files.isDirectory(directory)) {
try {
Files.createDirectories(directory);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return directory;
}

View File

@ -1,13 +1,11 @@
package com.sismics.docs.core.util;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
@ -132,7 +130,7 @@ public class FileUtil {
*/
public static void save(InputStream inputStream, File file, String privateKey) throws Exception {
Cipher cipher = EncryptionUtil.getEncryptionCipher(privateKey);
Path path = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId());
Path path = DirectoryUtil.getStorageDirectory().resolve(file.getId());
Files.copy(new CipherInputStream(inputStream, cipher), path);
// Generate file variations
@ -172,21 +170,15 @@ public class FileUtil {
image.flush();
// Write "web" encrypted image
java.io.File outputFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId() + "_web").toFile();
OutputStream outputStream = new CipherOutputStream(new FileOutputStream(outputFile), cipher);
try {
Path outputFile = DirectoryUtil.getStorageDirectory().resolve(file.getId() + "_web");
try (OutputStream outputStream = new CipherOutputStream(Files.newOutputStream(outputFile), cipher)) {
ImageUtil.writeJpeg(web, outputStream);
} finally {
outputStream.close();
}
// Write "thumb" encrypted image
outputFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId() + "_thumb").toFile();
outputStream = new CipherOutputStream(new FileOutputStream(outputFile), cipher);
try {
outputFile = DirectoryUtil.getStorageDirectory().resolve(file.getId() + "_thumb");
try (OutputStream outputStream = new CipherOutputStream(Files.newOutputStream(outputFile), cipher)) {
ImageUtil.writeJpeg(thumbnail, outputStream);
} finally {
outputStream.close();
}
}
}
@ -195,20 +187,21 @@ public class FileUtil {
* Remove a file from the storage filesystem.
*
* @param file File to delete
* @throws IOException
*/
public static void delete(File file) {
java.io.File storedFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId()).toFile();
java.io.File webFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId() + "_web").toFile();
java.io.File thumbnailFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId() + "_thumb").toFile();
public static void delete(File file) throws IOException {
Path storedFile = DirectoryUtil.getStorageDirectory().resolve(file.getId());
Path webFile = DirectoryUtil.getStorageDirectory().resolve(file.getId() + "_web");
Path thumbnailFile = DirectoryUtil.getStorageDirectory().resolve(file.getId() + "_thumb");
if (storedFile.exists()) {
storedFile.delete();
if (Files.exists(storedFile)) {
Files.delete(storedFile);
}
if (webFile.exists()) {
webFile.delete();
if (Files.exists(webFile)) {
Files.delete(webFile);
}
if (thumbnailFile.exists()) {
thumbnailFile.delete();
if (Files.exists(thumbnailFile)) {
Files.delete(thumbnailFile);
}
}
}

View File

@ -1,97 +0,0 @@
package com.sismics.docs.core.util.math;
/**
* Classe utilitaire pour les calculs
*
* @author bgamard
*
*/
public class MathUtil {
/**
* Arrondi à 2 décimales près
*
* @param d Nombre à arrondir
* @return Nombre arrondi
*/
public static Double round(Double d) {
return Math.round(d * 100.0) / 100.0;
}
/**
* Contraint une valeur entre min et max.
*
* @param value Valeur
* @param min Minimum
* @param max Maximum
* @return Valeur contrainte
*/
public static double clip(double value, double min, double max) {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
/**
* Interpole une valeur entre deux points.
*
* @param x Valeur à interpoler
* @param x1 Point 1 (x)
* @param y1 Point 1 (y)
* @param x2 Point 2 (x)
* @param y2 Point 2 (y)
* @return Valeur interpolée
*/
public static double interpolate(double x, double x1, double y1, double x2, double y2) {
double alpha = (x - x1) / (x2 - x1);
return y1 * (1 - alpha) + y2 * alpha;
}
/**
* Retourne un Double depuis un Number.
*
* @param number Number
* @return Double
*/
public static Double getDoubleFromNumber(Number number) {
if (number == null) {
return null;
}
return number.doubleValue();
}
/**
* Retourne un Integer depuis un Number.
*
* @param number Number
* @return Integer
*/
public static Integer getIntegerFromNumber(Number number) {
if (number == null) {
return null;
}
return number.intValue();
}
/**
* Retourne un Long depuis un Number.
*
* @param number Number
* @return Long
*/
public static Long getLongFromNumber(Number number) {
if (number == null) {
return null;
}
return number.longValue();
}
}

View File

@ -1,6 +1,16 @@
package com.sismics.util.jpa;
import com.sismics.docs.core.util.DirectoryUtil;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import org.hibernate.cfg.Environment;
import org.hibernate.internal.util.config.ConfigurationHelper;
import org.hibernate.service.ServiceRegistry;
@ -8,15 +18,7 @@ import org.hibernate.service.ServiceRegistryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import com.sismics.docs.core.util.DirectoryUtil;
/**
* Entity manager factory.
@ -79,8 +81,8 @@ public final class EMF {
log.info("Configuring EntityManager from environment parameters");
Map<Object, Object> props = new HashMap<Object, Object>();
props.put("hibernate.connection.driver_class", "org.h2.Driver");
File dbDirectory = DirectoryUtil.getDbDirectory();
String dbFile = dbDirectory.getAbsoluteFile() + File.separator + "docs";
Path dbDirectory = DirectoryUtil.getDbDirectory();
String dbFile = dbDirectory.resolve("docs").toAbsolutePath().toString();
props.put("hibernate.connection.url", "jdbc:h2:file:" + dbFile + ";CACHE_SIZE=65536");
props.put("hibernate.connection.username", "sa");
props.put("hibernate.hbm2ddl.auto", "none");

View File

@ -1 +1 @@
db.version=3
db.version=4

View File

@ -0,0 +1,3 @@
alter table T_USER add column USE_STORAGEQUOTA_N bigint not null default 10000000000;
alter table T_USER add column USE_STORAGECURRENT_N bigint not null default 0;
update T_CONFIG set CFG_VALUE_C = '4' where CFG_ID_C = 'DB_VERSION';

View File

@ -20,6 +20,8 @@ public class TestJpa extends BaseTransactionalTest {
user.setUsername("username");
user.setEmail("toto@docs.com");
user.setRoleId("admin");
user.setStorageCurrent(0l);
user.setStorageQuota(10l);
user.setPrivateKey("AwesomePrivateKey");
String id = userDao.create(user);

View File

@ -48,6 +48,7 @@
<org.apache.maven.plugins.maven-release-plugin.version>2.5.2</org.apache.maven.plugins.maven-release-plugin.version>
<org.apache.maven.plugins.maven-resources-plugin.version>2.7</org.apache.maven.plugins.maven-resources-plugin.version>
<org.apache.maven.plugins.maven-war-plugin.version>2.6</org.apache.maven.plugins.maven-war-plugin.version>
<org.apache.maven.plugins.maven-surefire-plugin.version>2.18.1</org.apache.maven.plugins.maven-surefire-plugin.version>
<org.eclipse.jetty.jetty-maven-plugin.version>9.2.13.v20150730</org.eclipse.jetty.jetty-maven-plugin.version>
</properties>
@ -108,12 +109,17 @@
<version>${org.apache.maven.plugins.maven-war-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${org.apache.maven.plugins.maven-surefire-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${org.eclipse.jetty.jetty-maven-plugin.version}</version>
</plugin>
</plugins>
</build>

View File

@ -155,7 +155,23 @@ public class ValidationUtil {
try {
return Integer.valueOf(s);
} catch (NumberFormatException e) {
throw new ClientException("Validation Error", MessageFormat.format("{0} is not a number", name));
throw new ClientException("ValidationError", MessageFormat.format("{0} is not a number", name));
}
}
/**
* Checks if the string is a number.
*
* @param s String to validate
* @param name Name of the parameter
* @return Parsed number
* @throws ClientException
*/
public static Long validateLong(String s, String name) throws ClientException {
try {
return Long.valueOf(s);
} catch (NumberFormatException e) {
throw new ClientException("ValidationError", MessageFormat.format("{0} is not a number", name));
}
}

View File

@ -1,6 +1,5 @@
package com.sismics.util.filter;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
@ -44,20 +43,19 @@ public class RequestContextFilter implements Filter {
if (!filterConfig.getServletContext().getServerInfo().startsWith("Grizzly")) {
EnvironmentUtil.setWebappContext(true);
}
File baseDataDirectory = null;
try {
baseDataDirectory = DirectoryUtil.getBaseDataDirectory();
if (log.isInfoEnabled()) {
log.info(MessageFormat.format("Using base data directory: {0}", DirectoryUtil.getBaseDataDirectory()));
}
} catch (Exception e) {
log.error("Error initializing base data directory", e);
}
if (log.isInfoEnabled()) {
log.info(MessageFormat.format("Using base data directory: {0}", baseDataDirectory.toString()));
}
// Initialize file logger
RollingFileAppender fileAppender = new RollingFileAppender();
fileAppender.setName("FILE");
fileAppender.setFile(DirectoryUtil.getLogDirectory() + File.separator + "docs.log");
fileAppender.setFile(DirectoryUtil.getLogDirectory().resolve("docs.log").toString());
fileAppender.setLayout(new PatternLayout("%d{DATE} %p %l %m %n"));
fileAppender.setThreshold(Level.INFO);
fileAppender.setAppend(true);

View File

@ -1,8 +1,6 @@
package com.sismics.docs.rest;
import java.io.File;
import java.net.URI;
import java.net.URLDecoder;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.UriBuilder;
@ -63,8 +61,7 @@ public abstract class BaseJerseyTest extends JerseyTest {
clientUtil = new ClientUtil(target());
String httpRoot = URLDecoder.decode(new File(getClass().getResource("/").getFile()).getAbsolutePath(), "utf-8");
httpServer = HttpServer.createSimpleServer(httpRoot, "localhost", getPort());
httpServer = HttpServer.createSimpleServer(getClass().getResource("/").getFile(), "localhost", getPort());
WebappContext context = new WebappContext("GrizzlyContext", "/docs");
context.addFilter("requestContextFilter", RequestContextFilter.class)
.addMappingForUrlPatterns(null, "/*");

View File

@ -40,7 +40,7 @@ public class ClientUtil {
form.param("username", username);
form.param("email", username + "@docs.com");
form.param("password", "12345678");
form.param("time_zone", "Asia/Tokyo");
form.param("storage_quota", "1000000"); // 1MB quota
resource.path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminAuthenticationToken)
.put(Entity.form(form), JsonObject.class);

View File

@ -174,6 +174,15 @@
</webApp>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
</configuration>
</plugin>
</plugins>
</build>
</profile>

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=3
db.version=4

View File

@ -1,5 +1,8 @@
package com.sismics.docs.rest.resource;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -19,8 +22,10 @@ import org.apache.log4j.Appender;
import org.apache.log4j.Logger;
import com.sismics.docs.core.dao.jpa.FileDao;
import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.docs.core.util.ConfigUtil;
import com.sismics.docs.core.util.DirectoryUtil;
import com.sismics.docs.core.util.jpa.PaginatedList;
@ -165,13 +170,65 @@ public class AppResource extends BaseResource {
}
// Check if each stored file is valid
java.io.File[] storedFileList = DirectoryUtil.getStorageDirectory().listFiles();
for (java.io.File storedFile : storedFileList) {
String fileName = storedFile.getName();
String[] fileNameArray = fileName.split("_");
if (!fileMap.containsKey(fileNameArray[0])) {
storedFile.delete();
try (DirectoryStream<java.nio.file.Path> storedFileList = Files.newDirectoryStream(DirectoryUtil.getStorageDirectory())) {
for (java.nio.file.Path storedFile : storedFileList) {
String fileName = storedFile.getFileName().toString();
String[] fileNameArray = fileName.split("_");
if (!fileMap.containsKey(fileNameArray[0])) {
Files.delete(storedFile);
}
}
} catch (IOException e) {
throw new ServerException("FileError", "Error deleting orphan files", e);
}
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok");
return Response.ok().entity(response.build()).build();
}
/**
* Recompute the quota for each user.
*
* @return Response
*/
@POST
@Path("batch/recompute_quota")
public Response batchRecomputeQuota() {
if (!authenticate()) {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
// Get all files
FileDao fileDao = new FileDao();
List<File> fileList = fileDao.findAll();
// Count each file for the corresponding user quota
UserDao userDao = new UserDao();
Map<String, User> userMap = new HashMap<>();
for (File file : fileList) {
java.nio.file.Path storedFile = DirectoryUtil.getStorageDirectory().resolve(file.getId());
User user = null;
if (userMap.containsKey(file.getUserId())) {
user = userMap.get(file.getUserId());
} else {
user = userDao.getById(file.getUserId());
user.setStorageCurrent(0l);
userMap.put(user.getId(), user);
}
try {
user.setStorageCurrent(user.getStorageCurrent() + Files.size(storedFile));
} catch (IOException e) {
throw new ServerException("MissingFile", "File does not exist", e);
}
}
// Save all users
for (User user : userMap.values()) {
userDao.update(user);
}
// Always return OK

View File

@ -1,10 +1,10 @@
package com.sismics.docs.rest.resource;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
@ -123,6 +123,11 @@ public class FileResource extends BaseResource {
throw new ClientException("InvalidFileType", "File type not recognized");
}
// Validate quota
if (user.getStorageCurrent() + fileData.length > user.getStorageQuota()) {
throw new ClientException("QuotaReached", "Quota limit reached");
}
try {
// Get files of this document
FileDao fileDao = new FileDao();
@ -144,6 +149,10 @@ public class FileResource extends BaseResource {
// Save the file
FileUtil.save(fileInputStream, file, user.getPrivateKey());
// Update the user quota
user.setStorageCurrent(user.getStorageCurrent() + fileData.length);
userDao.update(user);
// Raise a new file created event if we have a document
if (documentId != null) {
FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent();
@ -156,7 +165,8 @@ public class FileResource extends BaseResource {
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok")
.add("id", fileId);
.add("id", fileId)
.add("size", fileData.length);
return Response.ok().entity(response.build()).build();
} catch (Exception e) {
throw new ServerException("FileError", "Error adding a file", e);
@ -206,8 +216,8 @@ public class FileResource extends BaseResource {
// Raise a new file created event (it wasn't sent during file creation)
try {
java.io.File storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), id).toFile();
InputStream fileInputStream = new FileInputStream(storedfile);
java.nio.file.Path storedFile = DirectoryUtil.getStorageDirectory().resolve(id);
InputStream fileInputStream = Files.newInputStream(storedFile);
final InputStream responseInputStream = EncryptionUtil.decryptInputStream(fileInputStream, user.getPrivateKey());
FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent();
fileCreatedAsyncEvent.setDocument(document);
@ -294,11 +304,16 @@ public class FileResource extends BaseResource {
JsonArrayBuilder files = Json.createArrayBuilder();
for (File fileDb : fileList) {
files.add(Json.createObjectBuilder()
.add("id", fileDb.getId())
.add("mimetype", fileDb.getMimeType())
.add("document_id", JsonUtil.nullable(fileDb.getDocumentId()))
.add("create_date", fileDb.getCreateDate().getTime()));
try {
files.add(Json.createObjectBuilder()
.add("id", fileDb.getId())
.add("mimetype", fileDb.getMimeType())
.add("document_id", JsonUtil.nullable(fileDb.getDocumentId()))
.add("create_date", fileDb.getCreateDate().getTime())
.add("size", Files.size(DirectoryUtil.getStorageDirectory().resolve(fileDb.getId()))));
} catch (IOException e) {
throw new ServerException("FileError", "Unable to get the size of " + fileDb.getId(), e);
}
}
JsonObjectBuilder response = Json.createObjectBuilder()
@ -341,6 +356,17 @@ public class FileResource extends BaseResource {
// Delete the file
fileDao.delete(file.getId());
// Update the user quota
UserDao userDao = new UserDao();
User user = userDao.getById(principal.getId());
java.nio.file.Path storedFile = DirectoryUtil.getStorageDirectory().resolve(id);
try {
user.setStorageCurrent(user.getStorageCurrent() - Files.size(storedFile));
userDao.update(user);
} catch (IOException e) {
// The file doesn't exists on disk, which is weird, but not fatal
}
// Raise a new file deleted event
FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent();
fileDeletedAsyncEvent.setFile(file);
@ -396,30 +422,33 @@ public class FileResource extends BaseResource {
// Get the stored file
java.io.File storedfile;
java.nio.file.Path storedFile;
String mimeType;
boolean decrypt = false;
if (size != null) {
storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), fileId + "_" + size).toFile();
storedFile = DirectoryUtil.getStorageDirectory().resolve(fileId + "_" + size);
mimeType = MimeType.IMAGE_JPEG; // Thumbnails are JPEG
decrypt = true; // Thumbnails are encrypted
if (!storedfile.exists()) {
storedfile = new java.io.File(getClass().getResource("/image/file.png").getFile());
if (!Files.exists(storedFile)) {
storedFile = Paths.get(getClass().getResource("/image/file.png").getFile());
mimeType = MimeType.IMAGE_PNG;
decrypt = false;
}
} else {
storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), fileId).toFile();
storedFile = DirectoryUtil.getStorageDirectory().resolve(fileId);
mimeType = file.getMimeType();
decrypt = true; // Original files are encrypted
}
// Stream the output and decrypt it if necessary
StreamingOutput stream;
// A file is always encrypted by the creator of it
User user = userDao.getById(file.getUserId());
// Write the decrypted file to the output
try {
InputStream fileInputStream = new FileInputStream(storedfile);
InputStream fileInputStream = Files.newInputStream(storedFile);
final InputStream responseInputStream = decrypt ?
EncryptionUtil.decryptInputStream(fileInputStream, user.getPrivateKey()) : fileInputStream;
@ -484,8 +513,8 @@ public class FileResource extends BaseResource {
// Add each file to the ZIP stream
int index = 0;
for (File file : fileList) {
java.io.File storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file.getId()).toFile();
InputStream fileInputStream = new FileInputStream(storedfile);
java.nio.file.Path storedfile = DirectoryUtil.getStorageDirectory().resolve(file.getId());
InputStream fileInputStream = Files.newInputStream(storedfile);
// Add the decrypted file to the ZIP stream
// Files are encrypted by the creator of them

View File

@ -64,7 +64,8 @@ public class UserResource extends BaseResource {
public Response register(
@FormParam("username") String username,
@FormParam("password") String password,
@FormParam("email") String email) {
@FormParam("email") String email,
@FormParam("storage_quota") String storageQuotaStr) {
if (!authenticate()) {
throw new ForbiddenClientException();
@ -76,6 +77,7 @@ public class UserResource extends BaseResource {
ValidationUtil.validateAlphanumeric(username, "username");
password = ValidationUtil.validateLength(password, "password", 8, 50);
email = ValidationUtil.validateLength(email, "email", 3, 50);
Long storageQuota = ValidationUtil.validateLong(storageQuotaStr, "storage_quota");
ValidationUtil.validateEmail(email, "email");
// Create the user
@ -84,6 +86,8 @@ public class UserResource extends BaseResource {
user.setUsername(username);
user.setPassword(password);
user.setEmail(email);
user.setStorageQuota(storageQuota);
user.setStorageCurrent(0l);
try {
user.setPrivateKey(EncryptionUtil.generatePrivateKey());
} catch (NoSuchAlgorithmException e) {
@ -135,9 +139,9 @@ public class UserResource extends BaseResource {
if (email != null) {
user.setEmail(email);
}
user = userDao.update(user);
// Change the password
if (StringUtils.isNotBlank(password)) {
user.setPassword(password);
userDao.updatePassword(user);
@ -162,7 +166,8 @@ public class UserResource extends BaseResource {
public Response update(
@PathParam("username") String username,
@FormParam("password") String password,
@FormParam("email") String email) {
@FormParam("email") String email,
@FormParam("storage_quota") String storageQuotaStr) {
if (!authenticate()) {
throw new ForbiddenClientException();
@ -184,11 +189,14 @@ public class UserResource extends BaseResource {
if (email != null) {
user.setEmail(email);
}
if (StringUtils.isNotBlank(storageQuotaStr)) {
Long storageQuota = ValidationUtil.validateLong(storageQuotaStr, "storage_quota");
user.setStorageQuota(storageQuota);
}
user = userDao.update(user);
// Change the password
if (StringUtils.isNotBlank(password)) {
// Change the password
user.setPassword(password);
userDao.updatePassword(user);
}
@ -406,7 +414,9 @@ public class UserResource extends BaseResource {
UserDao userDao = new UserDao();
User user = userDao.getById(principal.getId());
response.add("username", user.getUsername())
.add("email", user.getEmail());
.add("email", user.getEmail())
.add("storage_quota", user.getStorageQuota())
.add("storage_current", user.getStorageCurrent());
JsonArrayBuilder baseFunctions = Json.createArrayBuilder();
for (String baseFunction : ((UserPrincipal) principal).getBaseFunctionSet()) {
baseFunctions.add(baseFunction);
@ -441,7 +451,9 @@ public class UserResource extends BaseResource {
JsonObjectBuilder response = Json.createObjectBuilder()
.add("username", user.getUsername())
.add("email", user.getEmail());
.add("email", user.getEmail())
.add("storage_quota", user.getStorageQuota())
.add("storage_current", user.getStorageCurrent());
return Response.ok().entity(response.build()).build();
}
@ -477,6 +489,8 @@ public class UserResource extends BaseResource {
.add("id", userDto.getId())
.add("username", userDto.getUsername())
.add("email", userDto.getEmail())
.add("storage_quota", userDto.getStorageQuota())
.add("storage_current", userDto.getStorageCurrent())
.add("create_date", userDto.getCreateTimestamp()));
}

View File

@ -3,7 +3,7 @@
/**
* Document default controller.
*/
angular.module('docs').controller('DocumentDefault', function($scope, $state, Restangular, $upload) {
angular.module('docs').controller('DocumentDefault', function($scope, $rootScope, $state, Restangular, $upload) {
// Load app data
Restangular.one('app').get().then(function(data) {
$scope.app = data;
@ -73,7 +73,18 @@ angular.module('docs').controller('DocumentDefault', function($scope, $state, Re
newfile.progress = parseInt(100.0 * e.loaded / e.total);
})
.success(function (data) {
// Update local model with real data
newfile.id = data.id;
newfile.size = data.size;
// New file uploaded, increase used quota
$rootScope.userInfo.storage_current += data.size;
})
.error(function (data) {
newfile.status = 'Upload error';
if (data.type == 'QuotaReached') {
newfile.status += ' - Quota reached';
}
});
};
@ -90,7 +101,11 @@ angular.module('docs').controller('DocumentDefault', function($scope, $state, Re
$scope.deleteFile = function ($event, file) {
$event.stopPropagation();
Restangular.one('file', file.id).remove().then(function () {
Restangular.one('file', file.id).remove().then(function() {
// File deleted, decrease used quota
$rootScope.userInfo.storage_current -= file.size;
// Update local data
$scope.loadFiles();
});
return false;

View File

@ -142,8 +142,8 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
success: function(response) {
deferred.resolve(response);
},
error: function(jqXHR, textStatus, errorThrown) {
deferred.reject(errorThrown);
error: function(jqXHR) {
deferred.reject(jqXHR);
},
xhr: function() {
var myXhr = $.ajaxSettings.xhr();
@ -155,8 +155,23 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
// Update progress bar and title on progress
var startProgress = $scope.fileProgress;
deferred.promise.then(null, null, function(e) {
var done = 1 - (e.total - e.position) / e.total;
deferred.promise.then(function(data) {
// New file uploaded, increase used quota
$rootScope.userInfo.storage_current += data.size;
}, function(data) {
// Error uploading a file, we stop here
$scope.alerts.unshift({
type: 'danger',
msg: 'Document successfully ' + ($scope.isEdit() ? 'edited' : 'added') + ' but some files cannot be uploaded'
+ (data.responseJSON.type == 'QuotaReached' ? ' - Quota reached' : '')
});
// Reset view and title
$scope.fileIsUploading = false;
$scope.fileProgress = 0;
$rootScope.pageTitle = 'Sismics Docs';
}, function(e) {
var done = 1 - (e.total - e.loaded) / e.total;
var chunk = 100 / _.size($scope.newFiles);
$scope.fileProgress = startProgress + done * chunk;
$rootScope.pageTitle = Math.round($scope.fileProgress) + '% - Sismics Docs';
@ -170,7 +185,7 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
var then = function() {
key++;
if ($scope.newFiles[key]) {
sendFile(key).then(then); // TODO Handle upload error
sendFile(key).then(then);
} else {
$scope.fileIsUploading = false;
$scope.fileProgress = 0;

View File

@ -3,7 +3,7 @@
/**
* Document view content controller.
*/
angular.module('docs').controller('DocumentViewContent', function ($scope, $stateParams, Restangular, $dialog, $state, $upload) {
angular.module('docs').controller('DocumentViewContent', function ($scope, $rootScope, $stateParams, Restangular, $dialog, $state, $upload) {
/**
* Configuration for file sorting.
*/
@ -55,6 +55,10 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $stat
$dialog.messageBox(title, msg, btns, function (result) {
if (result == 'ok') {
Restangular.one('file', file.id).remove().then(function () {
// File deleted, decrease used quota
$rootScope.userInfo.storage_current -= file.size;
// Update local data
$scope.loadFiles();
});
}
@ -109,11 +113,22 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $stat
id: $stateParams.id
}
})
.progress(function (e) {
.progress(function(e) {
newfile.progress = parseInt(100.0 * e.loaded / e.total);
})
.success(function (data) {
.success(function(data) {
// Update local model with real data
newfile.id = data.id;
newfile.size = data.size;
// New file uploaded, increase used quota
$rootScope.userInfo.storage_current += data.size;
})
.error(function (data) {
newfile.status = 'Upload error';
if (data.type == 'QuotaReached') {
newfile.status += ' - Quota reached';
}
});
};
});

View File

@ -16,6 +16,7 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog,
*/
if ($scope.isEdit()) {
Restangular.one('user', $stateParams.username).get().then(function(data) {
data.storage_quota /= 1000000;
$scope.user = data;
});
}
@ -25,15 +26,17 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog,
*/
$scope.edit = function() {
var promise = null;
var user = angular.copy($scope.user);
user.storage_quota *= 1000000;
if ($scope.isEdit()) {
promise = Restangular
.one('user', $stateParams.username)
.post('', $scope.user);
.post('', user);
} else {
promise = Restangular
.one('user')
.put($scope.user);
.put(user);
}
promise.then(function() {

View File

@ -71,7 +71,7 @@
<!-- endref -->
</head>
<body>
<nav class="navbar navbar-default" role="navigation" ng-controller="Navigation">
<nav class="navbar navbar-inverse" role="navigation" ng-controller="Navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
ng-init="isCollapsed = true"
@ -110,7 +110,10 @@
</a>
</li>
<li>
<a href="#/settings/account" title="Logged in as {{ userInfo.username }}">{{ userInfo.username }}</a>
<a href="#/settings/account" title="Logged in as {{ userInfo.username }}">
<span class="glyphicon glyphicon-user"></span>
{{ userInfo.username }}
</a>
</li>
<li ng-class="{active: $uiRoute}" ui-route="/settings.*">
<a href="#/settings/account">

View File

@ -2,7 +2,7 @@
<div ng-show="app">
<div class="well">
<h3>Quick upload</h3>
<h3><span class="glyphicon glyphicon-cloud-upload"></span> Quick upload</h3>
<div class="row upload-zone" ng-model="dropFiles" ng-file-drop drag-over-class="bg-success"
ng-multiple="true" allow-dir="false" accept="image/*,application/pdf,application/zip" ng-file-change="fileDropped($files, $event, $rejectedFiles)">
<div class="col-xs-6 col-sm-4 col-md-3 col-lg-2 text-center" ng-repeat="file in files">
@ -44,8 +44,8 @@
<div ui-view="file"></div>
<div class="well">
<h3>Latest activity</h3>
<div>
<h3><span class="glyphicon glyphicon-tasks"></span> Latest activity</h3>
<audit-log logs="logs" />
</div>

View File

@ -73,6 +73,11 @@
</select>
</div>
<div class="pull-left" title="To upgrade your quota, ask your administrator">
{{ userInfo.storage_current / 1000000 | number: 0 }}MB ({{ userInfo.storage_current / userInfo.storage_quota * 100 | number: 1 }}%)
used on {{ userInfo.storage_quota / 1000000 | number: 0 }}MB
</div>
<div class="text-right">
{{ totalDocuments }} document{{ totalDocuments > 1 ? 's' : '' }} found
</div>

View File

@ -37,6 +37,21 @@
<span class="help-block" ng-show="editUserForm.email.$error.maxlength">Too long</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !editUserForm.storage_quota.$valid, success: editUserForm.storage_quota.$valid }">
<label class="col-sm-2 control-label" for="inputQuota">Storage quota</label>
<div class="col-sm-7">
<div class="input-group">
<input name="storage_quota" type="number" id="inputQuota" required class="form-control"
ng-pattern="/[0-9]*/" placeholder="Storage quota (in MB)" ng-model="user.storage_quota"/>
<div class="input-group-addon">MB</div>
</div>
</div>
<div class="col-sm-3">
<span class="help-block" ng-show="editUserForm.storage_quota.$error.pattern">Number required</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !editUserForm.password.$valid, success: editUserForm.password.$valid }">
<label class="col-sm-2 control-label" for="inputPassword">Password</label>

View File

@ -7,6 +7,11 @@
}
}
// Navbar color
.navbar {
background-color: #263238;
}
// Documents list
.table-documents {
thead th {

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=3
db.version=4

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=3
db.version=4

View File

@ -53,6 +53,12 @@ public class TestAppResource extends BaseJerseyTest {
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminAuthenticationToken)
.post(Entity.form(new Form()));
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
// Recompute quota
response = target().path("/app/batch/recompute_quota").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminAuthenticationToken)
.post(Entity.form(new Form()));
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
}
/**

View File

@ -1,7 +1,6 @@
package com.sismics.docs.rest;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.Date;
import javax.json.JsonArray;
@ -237,9 +236,9 @@ public class TestDocumentResource extends BaseJerseyTest {
Assert.assertEquals("ok", json.getString("status"));
// Check that the associated files are deleted from FS
java.io.File storedFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id).toFile();
java.io.File webFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_web").toFile();
java.io.File thumbnailFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_thumb").toFile();
java.io.File storedFile = DirectoryUtil.getStorageDirectory().resolve(file1Id).toFile();
java.io.File webFile = DirectoryUtil.getStorageDirectory().resolve(file1Id + "_web").toFile();
java.io.File thumbnailFile = DirectoryUtil.getStorageDirectory().resolve(file1Id + "_thumb").toFile();
Assert.assertFalse(storedFile.exists());
Assert.assertFalse(webFile.exists());
Assert.assertFalse(thumbnailFile.exists());

View File

@ -1,9 +1,9 @@
package com.sismics.docs.rest;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Date;
import javax.json.JsonArray;
@ -68,6 +68,7 @@ public class TestFileResource extends BaseJerseyTest {
MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class);
file1Id = json.getString("id");
Assert.assertNotNull(file1Id);
Assert.assertEquals(163510l, json.getJsonNumber("size").longValue());
}
}
@ -121,8 +122,8 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertTrue(fileBytes.length > 0);
// Check that the files are not readable directly from FS
java.io.File storedFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id).toFile();
try (InputStream storedFileInputStream = new BufferedInputStream(new FileInputStream(storedFile))) {
Path storedFile = DirectoryUtil.getStorageDirectory().resolve(file1Id);
try (InputStream storedFileInputStream = new BufferedInputStream(Files.newInputStream(storedFile))) {
Assert.assertNull(MimeTypeUtil.guessMimeType(storedFileInputStream));
}
@ -135,6 +136,7 @@ public class TestFileResource extends BaseJerseyTest {
JsonArray files = json.getJsonArray("files");
Assert.assertEquals(2, files.size());
Assert.assertEquals(file1Id, files.getJsonObject(0).getString("id"));
Assert.assertEquals(163510l, files.getJsonObject(0).getJsonNumber("size").longValue());
Assert.assertEquals(file2Id, files.getJsonObject(1).getString("id"));
// Reorder files
@ -179,12 +181,12 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertEquals(Status.NOT_FOUND, Status.fromStatusCode(response.getStatus()));
// Check that files are deleted from FS
storedFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id).toFile();
java.io.File webFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_web").toFile();
java.io.File thumbnailFile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), file1Id + "_thumb").toFile();
Assert.assertFalse(storedFile.exists());
Assert.assertFalse(webFile.exists());
Assert.assertFalse(thumbnailFile.exists());
storedFile = DirectoryUtil.getStorageDirectory().resolve(file1Id);
Path webFile = DirectoryUtil.getStorageDirectory().resolve(file1Id + "_web");
Path thumbnailFile = DirectoryUtil.getStorageDirectory().resolve(file1Id + "_thumb");
Assert.assertFalse(Files.exists(storedFile));
Assert.assertFalse(Files.exists(webFile));
Assert.assertFalse(Files.exists(thumbnailFile));
// Get all files from a document
json = target().path("/file/list")
@ -198,7 +200,7 @@ public class TestFileResource extends BaseJerseyTest {
@Test
public void testOrphanFile() throws Exception {
// Login file1
// Login file2
clientUtil.createUser("file2");
String file2AuthenticationToken = clientUtil.login("file2");
@ -280,4 +282,97 @@ public class TestFileResource extends BaseJerseyTest {
.delete(JsonObject.class);
Assert.assertEquals("ok", json.getString("status"));
}
@Test
public void testQuota() throws Exception {
// Login file_quota
clientUtil.createUser("file_quota");
String fileQuotaAuthenticationToken = clientUtil.login("file_quota");
// Add a file (292641 bytes large)
String file1Id = null;
try (InputStream is = Resources.getResource("file/Einstein-Roosevelt-letter.png").openStream()) {
StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "Einstein-Roosevelt-letter.png");
try (FormDataMultiPart multiPart = new FormDataMultiPart()) {
JsonObject json = target()
.register(MultiPartFeature.class)
.path("/file").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaAuthenticationToken)
.put(Entity.entity(multiPart.bodyPart(streamDataBodyPart),
MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class);
file1Id = json.getString("id");
Assert.assertNotNull(file1Id);
}
}
// Check current quota
JsonObject json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaAuthenticationToken)
.get(JsonObject.class);
Assert.assertEquals(292641l, json.getJsonNumber("storage_current").longValue());
// Add a file (292641 bytes large)
try (InputStream is = Resources.getResource("file/Einstein-Roosevelt-letter.png").openStream()) {
StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "Einstein-Roosevelt-letter.png");
try (FormDataMultiPart multiPart = new FormDataMultiPart()) {
target()
.register(MultiPartFeature.class)
.path("/file").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaAuthenticationToken)
.put(Entity.entity(multiPart.bodyPart(streamDataBodyPart),
MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class);
}
}
// Check current quota
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaAuthenticationToken)
.get(JsonObject.class);
Assert.assertEquals(585282l, json.getJsonNumber("storage_current").longValue());
// Add a file (292641 bytes large)
try (InputStream is = Resources.getResource("file/Einstein-Roosevelt-letter.png").openStream()) {
StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "Einstein-Roosevelt-letter.png");
try (FormDataMultiPart multiPart = new FormDataMultiPart()) {
target()
.register(MultiPartFeature.class)
.path("/file").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaAuthenticationToken)
.put(Entity.entity(multiPart.bodyPart(streamDataBodyPart),
MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class);
}
}
// Check current quota
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaAuthenticationToken)
.get(JsonObject.class);
Assert.assertEquals(877923l, json.getJsonNumber("storage_current").longValue());
// Add a file (292641 bytes large)
try (InputStream is = Resources.getResource("file/Einstein-Roosevelt-letter.png").openStream()) {
StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "Einstein-Roosevelt-letter.png");
try (FormDataMultiPart multiPart = new FormDataMultiPart()) {
Response response = target()
.register(MultiPartFeature.class)
.path("/file").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaAuthenticationToken)
.put(Entity.entity(multiPart.bodyPart(streamDataBodyPart),
MediaType.MULTIPART_FORM_DATA_TYPE));
Assert.assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
}
}
// Deletes a file
json = target().path("/file/" + file1Id).request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaAuthenticationToken)
.delete(JsonObject.class);
Assert.assertEquals("ok", json.getString("status"));
// Check current quota
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaAuthenticationToken)
.get(JsonObject.class);
Assert.assertEquals(585282l, json.getJsonNumber("storage_current").longValue());
}
}

View File

@ -48,6 +48,13 @@ public class TestUserResource extends BaseJerseyTest {
.get(JsonObject.class);
JsonArray users = json.getJsonArray("users");
Assert.assertTrue(users.size() > 0);
JsonObject user = users.getJsonObject(0);
Assert.assertNotNull(user.getString("id"));
Assert.assertNotNull(user.getString("username"));
Assert.assertNotNull(user.getString("email"));
Assert.assertNotNull(user.getJsonNumber("storage_quota"));
Assert.assertNotNull(user.getJsonNumber("storage_current"));
Assert.assertNotNull(user.getJsonNumber("create_date"));
// Create a user KO (login length validation)
Response response = target().path("/user").request()
@ -55,7 +62,8 @@ public class TestUserResource extends BaseJerseyTest {
.put(Entity.form(new Form()
.param("username", " bb ")
.param("email", "bob@docs.com")
.param("password", "12345678")));
.param("password", "12345678")
.param("storage_quota", "10")));
Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus()));
json = response.readEntity(JsonObject.class);
Assert.assertEquals("ValidationError", json.getString("type"));
@ -67,11 +75,25 @@ public class TestUserResource extends BaseJerseyTest {
.put(Entity.form(new Form()
.param("username", "bob-")
.param("email", "bob@docs.com")
.param("password", "12345678")));
.param("password", "12345678")
.param("storage_quota", "10")));
Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus()));
json = response.readEntity(JsonObject.class);
Assert.assertEquals("ValidationError", json.getString("type"));
Assert.assertTrue(json.getString("message"), json.getString("message").contains("alphanumeric"));
// Create a user KO (invalid quota)
response = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminAuthenticationToken)
.put(Entity.form(new Form()
.param("username", "bob")
.param("email", "bob@docs.com")
.param("password", "12345678")
.param("storage_quota", "nope")));
Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus()));
json = response.readEntity(JsonObject.class);
Assert.assertEquals("ValidationError", json.getString("type"));
Assert.assertTrue(json.getString("message"), json.getString("message").contains("number"));
// Create a user KO (email format validation)
response = target().path("/user").request()
@ -79,7 +101,8 @@ public class TestUserResource extends BaseJerseyTest {
.put(Entity.form(new Form()
.param("username", "bob")
.param("email", "bobdocs.com")
.param("password", "12345678")));
.param("password", "12345678")
.param("storage_quota", "10")));
Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus()));
json = response.readEntity(JsonObject.class);
Assert.assertEquals("ValidationError", json.getString("type"));
@ -89,7 +112,8 @@ public class TestUserResource extends BaseJerseyTest {
Form form = new Form()
.param("username", " bob ")
.param("email", " bob@docs.com ")
.param("password", " 12345678 ");
.param("password", " 12345678 ")
.param("storage_quota", "10");
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminAuthenticationToken)
.put(Entity.form(form), JsonObject.class);
@ -154,6 +178,8 @@ public class TestUserResource extends BaseJerseyTest {
.get(JsonObject.class);
Assert.assertEquals("alice@docs.com", json.getString("email"));
Assert.assertFalse(json.getBoolean("is_default_password"));
Assert.assertEquals(0l, json.getJsonNumber("storage_current").longValue());
Assert.assertEquals(1000000l, json.getJsonNumber("storage_quota").longValue());
// Check bob user information
json = target().path("/user").request()
@ -219,6 +245,8 @@ public class TestUserResource extends BaseJerseyTest {
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminAuthenticationToken)
.get(JsonObject.class);
Assert.assertTrue(json.getBoolean("is_default_password"));
Assert.assertEquals(0l, json.getJsonNumber("storage_current").longValue());
Assert.assertEquals(10000000000l, json.getJsonNumber("storage_quota").longValue());
// User admin updates his information
json = target().path("/user").request()