Compare commits

...

7 Commits

Author SHA1 Message Date
Julien Kirch 48fcc13e8e Merge branch 'master' into better-query 2023-09-14 17:04:47 +02:00
Orland Karamani 1b382004cb
Albanian Language Support (#719)
Co-authored-by: Orlando Karamani <orlandothemover@gmail.com>
2023-09-14 16:51:11 +02:00
Julien Kirch ab7ff25929
Store file size in DB (#704) 2023-09-14 16:50:39 +02:00
Julien Kirch eedf19ad9d
Fix no favicon on shares #580 (#718) 2023-09-08 15:43:35 +02:00
Julien Kirch 941ace99c6
Fix typo in /file/:id/versions description (#717) 2023-09-07 16:46:43 +02:00
bgamard 95e0b870f6 Merge remote-tracking branch 'origin/master' 2023-06-29 21:33:12 +02:00
bgamard 2bdb2dc34f #678: reopen ldap connection for each login 2023-06-29 21:33:05 +02:00
41 changed files with 1303 additions and 219 deletions

View File

@ -31,7 +31,8 @@ RUN apt-get update && \
tesseract-ocr-tha \
tesseract-ocr-tur \
tesseract-ocr-ukr \
tesseract-ocr-vie && \
tesseract-ocr-vie \
tesseract-ocr-sqi && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
mkdir /app && \
cd /app && \

View File

@ -43,7 +43,7 @@ public class Constants {
/**
* Supported document languages.
*/
public static final List<String> SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor", "nld", "tur", "heb", "hun", "fin", "swe", "lav", "dan", "nor", "vie", "ces");
public static final List<String> SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor", "nld", "tur", "heb", "hun", "fin", "swe", "lav", "dan", "nor", "vie", "ces", "sqi");
/**
* Base URL environment variable.

View File

@ -163,6 +163,7 @@ public class FileDao {
fileDb.setMimeType(file.getMimeType());
fileDb.setVersionId(file.getVersionId());
fileDb.setLatestVersion(file.isLatestVersion());
fileDb.setSize(file.getSize());
return file;
}
@ -245,4 +246,12 @@ public class FileDao {
q.setParameter("versionId", versionId);
return q.getResultList();
}
public List<File> getFilesWithUnknownSize(int limit) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
TypedQuery<File> q = em.createQuery("select f from File f where f.size = :size and f.deleteDate is null order by f.order asc", File.class);
q.setParameter("size", File.UNKNOWN_SIZE);
q.setMaxResults(limit);
return q.getResultList();
}
}

View File

@ -13,6 +13,8 @@ public class FileDeletedAsyncEvent extends UserEvent {
*/
private String fileId;
private Long fileSize;
public String getFileId() {
return fileId;
}
@ -21,10 +23,19 @@ public class FileDeletedAsyncEvent extends UserEvent {
this.fileId = fileId;
}
public Long getFileSize() {
return fileSize;
}
public void setFileSize(Long fileSize) {
this.fileSize = fileSize;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("fileId", fileId)
.add("fileSize", fileSize)
.toString();
}
}
}

View File

@ -2,8 +2,11 @@ package com.sismics.docs.core.listener.async;
import com.google.common.eventbus.AllowConcurrentEvents;
import com.google.common.eventbus.Subscribe;
import com.sismics.docs.core.dao.UserDao;
import com.sismics.docs.core.event.FileDeletedAsyncEvent;
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.FileUtil;
import com.sismics.docs.core.util.TransactionUtil;
import org.slf4j.Logger;
@ -11,7 +14,7 @@ import org.slf4j.LoggerFactory;
/**
* Listener on file deleted.
*
*
* @author bgamard
*/
public class FileDeletedAsyncListener {
@ -22,7 +25,7 @@ public class FileDeletedAsyncListener {
/**
* File deleted.
*
*
* @param event File deleted event
* @throws Exception e
*/
@ -32,6 +35,24 @@ public class FileDeletedAsyncListener {
if (log.isInfoEnabled()) {
log.info("File deleted event: " + event.toString());
}
TransactionUtil.handle(() -> {
// Update the user quota
UserDao userDao = new UserDao();
User user = userDao.getById(event.getUserId());
if (user != null) {
Long fileSize = event.getFileSize();
if (fileSize.equals(File.UNKNOWN_SIZE)) {
// The file size was not in the database, in this case we need to get from the unencrypted size.
fileSize = FileUtil.getFileSize(event.getFileId(), user);
}
if (! fileSize.equals(File.UNKNOWN_SIZE)) {
user.setStorageCurrent(user.getStorageCurrent() - fileSize);
userDao.updateQuota(user);
}
}
});
// Delete the file from storage
FileUtil.delete(event.getFileId());

View File

@ -9,6 +9,7 @@ import com.sismics.docs.core.dao.UserDao;
import com.sismics.docs.core.listener.async.*;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.docs.core.service.FileService;
import com.sismics.docs.core.service.FileSizeService;
import com.sismics.docs.core.service.InboxService;
import com.sismics.docs.core.util.PdfUtil;
import com.sismics.docs.core.util.indexing.IndexingHandler;
@ -65,6 +66,11 @@ public class AppContext {
*/
private FileService fileService;
/**
* File size service.
*/
private FileSizeService fileSizeService;
/**
* Asynchronous executors.
*/
@ -102,6 +108,11 @@ public class AppContext {
inboxService.startAsync();
inboxService.awaitRunning();
// Start file size service
fileSizeService = new FileSizeService();
fileSizeService.startAsync();
fileSizeService.awaitRunning();
// Register fonts
PdfUtil.registerFonts();
@ -238,6 +249,10 @@ public class AppContext {
fileService.stopAsync();
}
if (fileSizeService != null) {
fileSizeService.stopAsync();
}
instance = null;
}
}

View File

@ -88,6 +88,14 @@ public class File implements Loggable {
@Column(name = "FIL_LATESTVERSION_B", nullable = false)
private boolean latestVersion;
public static final Long UNKNOWN_SIZE = -1L;
/**
* Can be {@link File#UNKNOWN_SIZE} if the size has not been stored in the database when the file has been uploaded
*/
@Column(name = "FIL_SIZE_N", nullable = false)
private Long size;
/**
* Private key to decrypt the file.
* Not saved to database, of course.
@ -204,6 +212,18 @@ public class File implements Loggable {
return this;
}
/**
* Can return {@link File#UNKNOWN_SIZE} if the file size is not stored in the database.
*/
public Long getSize() {
return size;
}
public File setSize(Long size) {
this.size = size;
return this;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)

View File

@ -0,0 +1,78 @@
package com.sismics.docs.core.service;
import com.google.common.util.concurrent.AbstractScheduledService;
import com.sismics.docs.core.dao.FileDao;
import com.sismics.docs.core.dao.UserDao;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.docs.core.util.FileUtil;
import com.sismics.docs.core.util.TransactionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Service that retrieve files sizes when they are not in the database.
*/
public class FileSizeService extends AbstractScheduledService {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(FileSizeService.class);
public FileSizeService() {
}
@Override
protected void startUp() {
log.info("File size service starting up");
}
@Override
protected void shutDown() {
log.info("File size service shutting down");
}
private static final int BATCH_SIZE = 30;
@Override
protected void runOneIteration() {
try {
TransactionUtil.handle(() -> {
FileDao fileDao = new FileDao();
List<File> files = fileDao.getFilesWithUnknownSize(BATCH_SIZE);
for(File file : files) {
processFile(file);
}
if(files.size() < BATCH_SIZE) {
log.info("No more file to process, stopping the service");
stopAsync();
}
});
} catch (Throwable e) {
log.error("Exception during file service iteration", e);
}
}
void processFile(File file) {
UserDao userDao = new UserDao();
User user = userDao.getById(file.getUserId());
if(user == null) {
return;
}
long fileSize = FileUtil.getFileSize(file.getId(), user);
if(fileSize != File.UNKNOWN_SIZE){
FileDao fileDao = new FileDao();
file.setSize(fileSize);
fileDao.update(file);
}
}
@Override
protected Scheduler scheduler() {
return Scheduler.newFixedDelaySchedule(0, 1, TimeUnit.MINUTES);
}
}

View File

@ -1,14 +1,11 @@
package com.sismics.docs.core.service;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.AbstractScheduledService;
import com.sismics.docs.core.constant.ConfigType;
import com.sismics.docs.core.dao.TagDao;
import com.sismics.docs.core.dao.criteria.TagCriteria;
import com.sismics.docs.core.dao.dto.TagDto;
import com.sismics.docs.core.event.DocumentCreatedAsyncEvent;
import com.sismics.docs.core.model.jpa.Config;
import com.sismics.docs.core.model.jpa.Document;
import com.sismics.docs.core.model.jpa.Tag;
import com.sismics.docs.core.util.ConfigUtil;

View File

@ -16,6 +16,9 @@ import com.sismics.util.Scalr;
import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.io.InputStreamReaderThread;
import com.sismics.util.mime.MimeTypeUtil;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.CountingInputStream;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -46,7 +49,7 @@ public class FileUtil {
/**
* File ID of files currently being processed.
*/
private static Set<String> processingFileSet = Collections.synchronizedSet(new HashSet<>());
private static final Set<String> processingFileSet = Collections.synchronizedSet(new HashSet<>());
/**
* Optical character recognition on an image.
@ -149,6 +152,7 @@ public class FileUtil {
file.setName(StringUtils.abbreviate(name, 200));
file.setMimeType(mimeType);
file.setUserId(userId);
file.setSize(fileSize);
// Get files of this document
FileDao fileDao = new FileDao();
@ -240,4 +244,31 @@ public class FileUtil {
public static boolean isProcessingFile(String fileId) {
return processingFileSet.contains(fileId);
}
/**
* Get the size of a file on disk.
*
* @param fileId the file id
* @param user the file owner
* @return the size or -1 if something went wrong
*/
public static long getFileSize(String fileId, User user) {
// To get the size we copy the decrypted content into a null output stream
// and count the copied byte size.
Path storedFile = DirectoryUtil.getStorageDirectory().resolve(fileId);
if (! Files.exists(storedFile)) {
log.debug("File does not exist " + fileId);
return File.UNKNOWN_SIZE;
}
try (InputStream fileInputStream = Files.newInputStream(storedFile);
InputStream inputStream = EncryptionUtil.decryptInputStream(fileInputStream, user.getPrivateKey());
CountingInputStream countingInputStream = new CountingInputStream(inputStream);
) {
IOUtils.copy(countingInputStream, NullOutputStream.NULL_OUTPUT_STREAM);
return countingInputStream.getByteCount();
} catch (Exception e) {
log.debug("Can't find size of file " + fileId, e);
return File.UNKNOWN_SIZE;
}
}
}

View File

@ -13,10 +13,9 @@ import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Value;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.ldap.client.api.DefaultLdapConnectionFactory;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.ldap.client.api.LdapConnectionConfig;
import org.apache.directory.ldap.client.api.LdapConnectionPool;
import org.apache.directory.ldap.client.api.ValidatingPoolableLdapConnectionFactory;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -35,32 +34,14 @@ public class LdapAuthenticationHandler implements AuthenticationHandler {
private static final Logger log = LoggerFactory.getLogger(LdapAuthenticationHandler.class);
/**
* LDAP connection pool.
* Get a LDAP connection.
* @return LdapConnection
*/
private static LdapConnectionPool pool;
/**
* Reset the LDAP pool.
*/
public static void reset() {
if (pool != null) {
try {
pool.close();
} catch (Exception e) {
// NOP
}
}
pool = null;
}
/**
* Initialize the LDAP pool.
*/
private static void init() {
private LdapConnection getConnection() {
ConfigDao configDao = new ConfigDao();
Config ldapEnabled = configDao.getById(ConfigType.LDAP_ENABLED);
if (pool != null || ldapEnabled == null || !Boolean.parseBoolean(ldapEnabled.getValue())) {
return;
if (ldapEnabled == null || !Boolean.parseBoolean(ldapEnabled.getValue())) {
return null;
}
LdapConnectionConfig config = new LdapConnectionConfig();
@ -70,25 +51,23 @@ public class LdapAuthenticationHandler implements AuthenticationHandler {
config.setName(ConfigUtil.getConfigStringValue(ConfigType.LDAP_ADMIN_DN));
config.setCredentials(ConfigUtil.getConfigStringValue(ConfigType.LDAP_ADMIN_PASSWORD));
DefaultLdapConnectionFactory factory = new DefaultLdapConnectionFactory(config);
pool = new LdapConnectionPool(new ValidatingPoolableLdapConnectionFactory(factory), null);
return new LdapNetworkConnection(config);
}
@Override
public User authenticate(String username, String password) {
init();
if (pool == null) {
return null;
}
// Fetch and authenticate the user
Entry userEntry;
try {
EntryCursor cursor = pool.getConnection().search(ConfigUtil.getConfigStringValue(ConfigType.LDAP_BASE_DN),
try (LdapConnection ldapConnection = getConnection()) {
if (ldapConnection == null) {
return null;
}
EntryCursor cursor = ldapConnection.search(ConfigUtil.getConfigStringValue(ConfigType.LDAP_BASE_DN),
ConfigUtil.getConfigStringValue(ConfigType.LDAP_FILTER).replace("USERNAME", username), SearchScope.SUBTREE);
if (cursor.next()) {
userEntry = cursor.get();
pool.getConnection().bind(userEntry.getDn(), password);
ldapConnection.bind(userEntry.getDn(), password);
} else {
// User not found
return null;

View File

@ -3,7 +3,6 @@ package com.sismics.docs.core.util.format;
import com.google.common.collect.Lists;
import com.sismics.util.ClasspathScanner;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
/**

View File

@ -1,6 +1,5 @@
package com.sismics.docs.core.util.format;
import com.google.common.base.Charsets;
import com.google.common.io.Closer;
import com.lowagie.text.*;
import com.lowagie.text.pdf.PdfWriter;

View File

@ -1 +1 @@
db.version=28
db.version=30

View File

@ -1,2 +1,2 @@
create index IDX_FIL_IDDOC_C ON T_FILE (FIL_IDDOC_C ASC);
alter table T_FILE add column FIL_SIZE_N bigint not null default -1;
update T_CONFIG set CFG_VALUE_C = '29' where CFG_ID_C = 'DB_VERSION';

View File

@ -0,0 +1,2 @@
create index IDX_FIL_IDDOC_C ON T_FILE (FIL_IDDOC_C ASC);
update T_CONFIG set CFG_VALUE_C = '30' where CFG_ID_C = 'DB_VERSION';

View File

@ -0,0 +1,49 @@
package com.sismics;
import java.io.InputStream;
import java.net.URL;
public abstract class BaseTest {
protected static final String FILE_CSV = "document.csv";
protected static final String FILE_DOCX = "document.docx";
protected static final String FILE_GIF = "image.gif";
protected static final String FILE_JPG = "apollo_portrait.jpg";
protected static final Long FILE_JPG_SIZE = 7_907L;
protected static final String FILE_JPG2 = "apollo_landscape.jpg";
protected static final String FILE_MP4 = "video.mp4";
protected static final String FILE_ODT = "document.odt";
protected static final String FILE_PDF = "udhr.pdf";
protected static final String FILE_PDF_ENCRYPTED = "udhr_encrypted.pdf";
protected static final String FILE_PDF_SCANNED = "scanned.pdf";
protected static final String FILE_PNG = "image.png";
protected static final String FILE_PPTX = "apache.pptx";
protected static final String FILE_TXT = "document.txt";
protected static final String FILE_WEBM = "video.webm";
protected static final String FILE_XLSX = "document.xlsx";
protected static final String FILE_ZIP = "document.zip";
protected static URL getResource(String fileName) {
return ClassLoader.getSystemResource("file/" + fileName);
}
protected static InputStream getSystemResourceAsStream(String fileName) {
return ClassLoader.getSystemResourceAsStream("file/" + fileName);
}
}

View File

@ -1,19 +1,34 @@
package com.sismics.docs;
import com.sismics.BaseTest;
import com.sismics.docs.core.dao.FileDao;
import com.sismics.docs.core.dao.UserDao;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.docs.core.util.DirectoryUtil;
import com.sismics.docs.core.util.EncryptionUtil;
import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.jpa.EMF;
import com.sismics.util.mime.MimeType;
import org.junit.After;
import org.junit.Before;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityTransaction;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
/**
* Base class of tests with a transactional context.
*
* @author jtremeaux
*/
public abstract class BaseTransactionalTest {
public abstract class BaseTransactionalTest extends BaseTest {
@Before
public void setUp() throws Exception {
// Initialize the entity manager
@ -27,4 +42,32 @@ public abstract class BaseTransactionalTest {
@After
public void tearDown() throws Exception {
}
protected User createUser(String userName) throws Exception {
UserDao userDao = new UserDao();
User user = new User();
user.setUsername(userName);
user.setPassword("12345678");
user.setEmail("toto@docs.com");
user.setRoleId("admin");
user.setStorageQuota(100_000L);
userDao.create(user, userName);
return user;
}
protected File createFile(User user, long fileSize) throws Exception {
FileDao fileDao = new FileDao();
try(InputStream inputStream = getSystemResourceAsStream(FILE_JPG)) {
File file = new File();
file.setId("apollo_portrait");
file.setUserId(user.getId());
file.setVersion(0);
file.setMimeType(MimeType.IMAGE_JPEG);
file.setSize(fileSize);
String fileId = fileDao.create(file, user.getId());
Cipher cipher = EncryptionUtil.getEncryptionCipher(user.getPrivateKey());
Files.copy(new CipherInputStream(inputStream, cipher), DirectoryUtil.getStorageDirectory().resolve(fileId), REPLACE_EXISTING);
return file;
}
}
}

View File

@ -18,22 +18,16 @@ public class TestJpa extends BaseTransactionalTest {
public void testJpa() throws Exception {
// Create a user
UserDao userDao = new UserDao();
User user = new User();
user.setUsername("username");
user.setPassword("12345678");
user.setEmail("toto@docs.com");
user.setRoleId("admin");
user.setStorageQuota(10L);
String id = userDao.create(user, "me");
User user = createUser("testJpa");
TransactionUtil.commit();
// Search a user by his ID
user = userDao.getById(id);
user = userDao.getById(user.getId());
Assert.assertNotNull(user);
Assert.assertEquals("toto@docs.com", user.getEmail());
// Authenticate using the database
Assert.assertNotNull(new InternalAuthenticationHandler().authenticate("username", "12345678"));
Assert.assertNotNull(new InternalAuthenticationHandler().authenticate("testJpa", "12345678"));
}
}

View File

@ -0,0 +1,52 @@
package com.sismics.docs.core.listener.async;
import com.sismics.docs.BaseTransactionalTest;
import com.sismics.docs.core.dao.UserDao;
import com.sismics.docs.core.event.FileDeletedAsyncEvent;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.docs.core.util.TransactionUtil;
import org.junit.Assert;
import org.junit.Test;
public class FileDeletedAsyncListenerTest extends BaseTransactionalTest {
@Test
public void updateQuotaSizeKnown() throws Exception {
User user = createUser("updateQuotaSizeKnown");
File file = createFile(user, FILE_JPG_SIZE);
UserDao userDao = new UserDao();
user = userDao.getById(user.getId());
user.setStorageCurrent(10_000L);
userDao.updateQuota(user);
FileDeletedAsyncListener fileDeletedAsyncListener = new FileDeletedAsyncListener();
TransactionUtil.commit();
FileDeletedAsyncEvent event = new FileDeletedAsyncEvent();
event.setFileSize(FILE_JPG_SIZE);
event.setFileId(file.getId());
event.setUserId(user.getId());
fileDeletedAsyncListener.on(event);
Assert.assertEquals(userDao.getById(user.getId()).getStorageCurrent(), Long.valueOf(10_000 - FILE_JPG_SIZE));
}
@Test
public void updateQuotaSizeUnknown() throws Exception {
User user = createUser("updateQuotaSizeUnknown");
File file = createFile(user, File.UNKNOWN_SIZE);
UserDao userDao = new UserDao();
user = userDao.getById(user.getId());
user.setStorageCurrent(10_000L);
userDao.updateQuota(user);
FileDeletedAsyncListener fileDeletedAsyncListener = new FileDeletedAsyncListener();
TransactionUtil.commit();
FileDeletedAsyncEvent event = new FileDeletedAsyncEvent();
event.setFileSize(FILE_JPG_SIZE);
event.setFileId(file.getId());
event.setUserId(user.getId());
fileDeletedAsyncListener.on(event);
Assert.assertEquals(userDao.getById(user.getId()).getStorageCurrent(), Long.valueOf(10_000 - FILE_JPG_SIZE));
}
}

View File

@ -0,0 +1,22 @@
package com.sismics.docs.core.service;
import com.sismics.docs.BaseTransactionalTest;
import com.sismics.docs.core.dao.FileDao;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.model.jpa.User;
import org.junit.Assert;
import org.junit.Test;
public class TestFileSizeService extends BaseTransactionalTest {
@Test
public void processFileTest() throws Exception {
User user = createUser("processFileTest");
FileDao fileDao = new FileDao();
File file = createFile(user, File.UNKNOWN_SIZE);
FileSizeService fileSizeService = new FileSizeService();
fileSizeService.processFile(file);
Assert.assertEquals(fileDao.getFile(file.getId()).getSize(), Long.valueOf(FILE_JPG_SIZE));
}
}

View File

@ -2,6 +2,7 @@ package com.sismics.docs.core.util;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import com.sismics.BaseTest;
import org.junit.Assert;
import org.junit.Test;
@ -14,7 +15,7 @@ import java.io.InputStream;
*
* @author bgamard
*/
public class TestEncryptUtil {
public class TestEncryptUtil extends BaseTest {
@Test
public void generatePrivateKeyTest() {
String key = EncryptionUtil.generatePrivateKey();
@ -31,9 +32,9 @@ public class TestEncryptUtil {
// NOP
}
Cipher cipher = EncryptionUtil.getEncryptionCipher("OnceUponATime");
InputStream inputStream = new CipherInputStream(this.getClass().getResourceAsStream("/file/udhr.pdf"), cipher);
InputStream inputStream = new CipherInputStream(getSystemResourceAsStream(FILE_PDF), cipher);
byte[] encryptedData = ByteStreams.toByteArray(inputStream);
byte[] assertData = ByteStreams.toByteArray(this.getClass().getResourceAsStream("/file/udhr_encrypted.pdf"));
byte[] assertData = ByteStreams.toByteArray(getSystemResourceAsStream(FILE_PDF_ENCRYPTED));
Assert.assertEquals(encryptedData.length, assertData.length);
}
@ -41,9 +42,9 @@ public class TestEncryptUtil {
@Test
public void decryptStreamTest() throws Exception {
InputStream inputStream = EncryptionUtil.decryptInputStream(
this.getClass().getResourceAsStream("/file/udhr_encrypted.pdf"), "OnceUponATime");
getSystemResourceAsStream(FILE_PDF_ENCRYPTED), "OnceUponATime");
byte[] encryptedData = ByteStreams.toByteArray(inputStream);
byte[] assertData = ByteStreams.toByteArray(this.getClass().getResourceAsStream("/file/udhr.pdf"));
byte[] assertData = ByteStreams.toByteArray(getSystemResourceAsStream(FILE_PDF));
Assert.assertEquals(encryptedData.length, assertData.length);
}

View File

@ -2,6 +2,7 @@ package com.sismics.docs.core.util;
import com.google.common.collect.Lists;
import com.google.common.io.Resources;
import com.sismics.BaseTest;
import com.sismics.docs.core.dao.dto.DocumentDto;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.util.format.*;
@ -23,11 +24,11 @@ import java.util.Date;
*
* @author bgamard
*/
public class TestFileUtil {
public class TestFileUtil extends BaseTest {
@Test
public void extractContentOpenDocumentTextTest() throws Exception {
Path path = Paths.get(ClassLoader.getSystemResource("file/document.odt").toURI());
FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, "document.odt"));
Path path = Paths.get(getResource(FILE_ODT).toURI());
FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, FILE_ODT));
Assert.assertNotNull(formatHandler);
Assert.assertTrue(formatHandler instanceof OdtFormatHandler);
String content = formatHandler.extractContent("eng", path);
@ -36,8 +37,8 @@ public class TestFileUtil {
@Test
public void extractContentOfficeDocumentTest() throws Exception {
Path path = Paths.get(ClassLoader.getSystemResource("file/document.docx").toURI());
FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, "document.docx"));
Path path = Paths.get(getResource(FILE_DOCX).toURI());
FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, FILE_DOCX));
Assert.assertNotNull(formatHandler);
Assert.assertTrue(formatHandler instanceof DocxFormatHandler);
String content = formatHandler.extractContent("eng", path);
@ -46,8 +47,8 @@ public class TestFileUtil {
@Test
public void extractContentPowerpointTest() throws Exception {
Path path = Paths.get(ClassLoader.getSystemResource("file/apache.pptx").toURI());
FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, "apache.pptx"));
Path path = Paths.get(getResource(FILE_PPTX).toURI());
FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, FILE_PPTX));
Assert.assertNotNull(formatHandler);
Assert.assertTrue(formatHandler instanceof PptxFormatHandler);
String content = formatHandler.extractContent("eng", path);
@ -56,8 +57,8 @@ public class TestFileUtil {
@Test
public void extractContentPdf() throws Exception {
Path path = Paths.get(ClassLoader.getSystemResource("file/udhr.pdf").toURI());
FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, "udhr.pdf"));
Path path = Paths.get(getResource(FILE_PDF).toURI());
FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, FILE_PDF));
Assert.assertNotNull(formatHandler);
Assert.assertTrue(formatHandler instanceof PdfFormatHandler);
String content = formatHandler.extractContent("eng", path);
@ -66,8 +67,8 @@ public class TestFileUtil {
@Test
public void extractContentScannedPdf() throws Exception {
Path path = Paths.get(ClassLoader.getSystemResource("file/scanned.pdf").toURI());
FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, "scanned.pdf"));
Path path = Paths.get(getResource("scanned.pdf").toURI());
FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, FILE_PDF_SCANNED));
Assert.assertNotNull(formatHandler);
Assert.assertTrue(formatHandler instanceof PdfFormatHandler);
String content = formatHandler.extractContent("eng", path);
@ -76,12 +77,12 @@ public class TestFileUtil {
@Test
public void convertToPdfTest() throws Exception {
try (InputStream inputStream0 = Resources.getResource("file/apollo_landscape.jpg").openStream();
InputStream inputStream1 = Resources.getResource("file/apollo_portrait.jpg").openStream();
InputStream inputStream2 = Resources.getResource("file/udhr_encrypted.pdf").openStream();
InputStream inputStream3 = Resources.getResource("file/document.docx").openStream();
InputStream inputStream4 = Resources.getResource("file/document.odt").openStream();
InputStream inputStream5 = Resources.getResource("file/apache.pptx").openStream()) {
try (InputStream inputStream0 = getSystemResourceAsStream(FILE_JPG2);
InputStream inputStream1 = getSystemResourceAsStream(FILE_JPG);
InputStream inputStream2 = getSystemResourceAsStream(FILE_PDF_ENCRYPTED);
InputStream inputStream3 = getSystemResourceAsStream(FILE_DOCX);
InputStream inputStream4 = getSystemResourceAsStream(FILE_ODT);
InputStream inputStream5 = getSystemResourceAsStream(FILE_PPTX)) {
// Document
DocumentDto documentDto = new DocumentDto();
documentDto.setTitle("My super document 1");

View File

@ -1,5 +1,6 @@
package com.sismics.util;
import com.sismics.BaseTest;
import com.sismics.util.mime.MimeType;
import com.sismics.util.mime.MimeTypeUtil;
import org.junit.Assert;
@ -13,59 +14,59 @@ import java.nio.file.Paths;
*
* @author bgamard
*/
public class TestMimeTypeUtil {
public class TestMimeTypeUtil extends BaseTest {
@Test
public void test() throws Exception {
// Detect ODT files
Path path = Paths.get(ClassLoader.getSystemResource("file/document.odt").toURI());
Assert.assertEquals(MimeType.OPEN_DOCUMENT_TEXT, MimeTypeUtil.guessMimeType(path, "document.odt"));
Path path = Paths.get(getResource(FILE_ODT).toURI());
Assert.assertEquals(MimeType.OPEN_DOCUMENT_TEXT, MimeTypeUtil.guessMimeType(path, FILE_ODT));
// Detect DOCX files
path = Paths.get(ClassLoader.getSystemResource("file/document.docx").toURI());
Assert.assertEquals(MimeType.OFFICE_DOCUMENT, MimeTypeUtil.guessMimeType(path, "document.odt"));
path = Paths.get(getResource(FILE_DOCX).toURI());
Assert.assertEquals(MimeType.OFFICE_DOCUMENT, MimeTypeUtil.guessMimeType(path, FILE_ODT));
// Detect PPTX files
path = Paths.get(ClassLoader.getSystemResource("file/apache.pptx").toURI());
Assert.assertEquals(MimeType.OFFICE_PRESENTATION, MimeTypeUtil.guessMimeType(path, "apache.pptx"));
path = Paths.get(getResource(FILE_PPTX).toURI());
Assert.assertEquals(MimeType.OFFICE_PRESENTATION, MimeTypeUtil.guessMimeType(path, FILE_PPTX));
// Detect XLSX files
path = Paths.get(ClassLoader.getSystemResource("file/document.xlsx").toURI());
Assert.assertEquals(MimeType.OFFICE_SHEET, MimeTypeUtil.guessMimeType(path, "document.xlsx"));
path = Paths.get(getResource(FILE_XLSX).toURI());
Assert.assertEquals(MimeType.OFFICE_SHEET, MimeTypeUtil.guessMimeType(path, FILE_XLSX));
// Detect TXT files
path = Paths.get(ClassLoader.getSystemResource("file/document.txt").toURI());
Assert.assertEquals(MimeType.TEXT_PLAIN, MimeTypeUtil.guessMimeType(path, "document.txt"));
path = Paths.get(getResource(FILE_TXT).toURI());
Assert.assertEquals(MimeType.TEXT_PLAIN, MimeTypeUtil.guessMimeType(path, FILE_TXT));
// Detect CSV files
path = Paths.get(ClassLoader.getSystemResource("file/document.csv").toURI());
Assert.assertEquals(MimeType.TEXT_CSV, MimeTypeUtil.guessMimeType(path, "document.csv"));
path = Paths.get(getResource(FILE_CSV).toURI());
Assert.assertEquals(MimeType.TEXT_CSV, MimeTypeUtil.guessMimeType(path, FILE_CSV));
// Detect PDF files
path = Paths.get(ClassLoader.getSystemResource("file/udhr.pdf").toURI());
Assert.assertEquals(MimeType.APPLICATION_PDF, MimeTypeUtil.guessMimeType(path, "udhr.pdf"));
path = Paths.get(getResource(FILE_PDF).toURI());
Assert.assertEquals(MimeType.APPLICATION_PDF, MimeTypeUtil.guessMimeType(path, FILE_PDF));
// Detect JPEG files
path = Paths.get(ClassLoader.getSystemResource("file/apollo_portrait.jpg").toURI());
Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(path, "apollo_portrait.jpg"));
path = Paths.get(getResource(FILE_JPG).toURI());
Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(path, FILE_JPG));
// Detect GIF files
path = Paths.get(ClassLoader.getSystemResource("file/image.gif").toURI());
Assert.assertEquals(MimeType.IMAGE_GIF, MimeTypeUtil.guessMimeType(path, "image.gif"));
path = Paths.get(getResource(FILE_GIF).toURI());
Assert.assertEquals(MimeType.IMAGE_GIF, MimeTypeUtil.guessMimeType(path, FILE_GIF));
// Detect PNG files
path = Paths.get(ClassLoader.getSystemResource("file/image.png").toURI());
Assert.assertEquals(MimeType.IMAGE_PNG, MimeTypeUtil.guessMimeType(path, "image.png"));
path = Paths.get(getResource(FILE_PNG).toURI());
Assert.assertEquals(MimeType.IMAGE_PNG, MimeTypeUtil.guessMimeType(path, FILE_PNG));
// Detect ZIP files
path = Paths.get(ClassLoader.getSystemResource("file/document.zip").toURI());
Assert.assertEquals(MimeType.APPLICATION_ZIP, MimeTypeUtil.guessMimeType(path, "document.zip"));
path = Paths.get(getResource(FILE_ZIP).toURI());
Assert.assertEquals(MimeType.APPLICATION_ZIP, MimeTypeUtil.guessMimeType(path, FILE_ZIP));
// Detect WEBM files
path = Paths.get(ClassLoader.getSystemResource("file/video.webm").toURI());
Assert.assertEquals(MimeType.VIDEO_WEBM, MimeTypeUtil.guessMimeType(path, "video.webm"));
path = Paths.get(getResource(FILE_WEBM).toURI());
Assert.assertEquals(MimeType.VIDEO_WEBM, MimeTypeUtil.guessMimeType(path, FILE_WEBM));
// Detect MP4 files
path = Paths.get(ClassLoader.getSystemResource("file/video.mp4").toURI());
Assert.assertEquals(MimeType.VIDEO_MP4, MimeTypeUtil.guessMimeType(path, "video.mp4"));
path = Paths.get(getResource(FILE_MP4).toURI());
Assert.assertEquals(MimeType.VIDEO_MP4, MimeTypeUtil.guessMimeType(path, FILE_MP4));
}
}

View File

@ -1,5 +1,6 @@
package com.sismics.util.format;
import com.sismics.BaseTest;
import com.sismics.docs.core.util.format.PdfFormatHandler;
import org.junit.Assert;
import org.junit.Test;
@ -11,14 +12,14 @@ import java.nio.file.Paths;
*
* @author bgamard
*/
public class TestPdfFormatHandler {
public class TestPdfFormatHandler extends BaseTest {
/**
* Test related to https://github.com/sismics/docs/issues/373.
*/
@Test
public void testIssue373() throws Exception {
PdfFormatHandler formatHandler = new PdfFormatHandler();
String content = formatHandler.extractContent("deu", Paths.get(ClassLoader.getSystemResource("file/issue373.pdf").toURI()));
String content = formatHandler.extractContent("deu", Paths.get(getResource("issue373.pdf").toURI()));
Assert.assertTrue(content.contains("Aufrechterhaltung"));
Assert.assertTrue(content.contains("Außentemperatur"));
Assert.assertTrue(content.contains("Grundumsatzmessungen"));

View File

@ -8,6 +8,7 @@ import com.sismics.util.JsonUtil;
import jakarta.json.Json;
import jakarta.json.JsonObjectBuilder;
import java.io.IOException;
import java.nio.file.Files;
@ -18,12 +19,15 @@ import java.nio.file.Files;
*/
public class RestUtil {
/**
* Transform a File into its JSON representation
* Transform a File into its JSON representation.
* If the file size it is not stored in the database the size can be wrong
* because the encrypted file size is used.
* @param fileDb a file
* @return the JSON
*/
public static JsonObjectBuilder fileToJsonObjectBuilder(File fileDb) {
try {
long fileSize = fileDb.getSize().equals(File.UNKNOWN_SIZE) ? Files.size(DirectoryUtil.getStorageDirectory().resolve(fileDb.getId())) : fileDb.getSize();
return Json.createObjectBuilder()
.add("id", fileDb.getId())
.add("processing", FileUtil.isProcessingFile(fileDb.getId()))
@ -32,7 +36,7 @@ public class RestUtil {
.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())));
.add("size", fileSize);
} catch (IOException e) {
throw new ServerException("FileError", "Unable to get the size of " + fileDb.getId(), e);
}

View File

@ -25,6 +25,9 @@ import jakarta.ws.rs.core.UriBuilder;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
@ -39,7 +42,9 @@ public abstract class BaseJerseyTest extends JerseyTest {
protected static final String FILE_DOCUMENT_ODT = "file/document.odt";
protected static final String FILE_DOCUMENT_TXT = "file/document.txt";
protected static final String FILE_EINSTEIN_ROOSEVELT_LETTER_PNG = "file/Einstein-Roosevelt-letter.png";
protected static final long FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE = 292641L;
protected static final String FILE_PIA_00452_JPG = "file/PIA00452.jpg";
protected static final long FILE_PIA_00452_JPG_SIZE = 163510L;
protected static final String FILE_VIDEO_WEBM = "file/video.webm";
protected static final String FILE_WIKIPEDIA_PDF = "file/wikipedia.pdf";
protected static final String FILE_WIKIPEDIA_ZIP = "file/wikipedia.zip";

View File

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

View File

@ -14,7 +14,6 @@ import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.service.InboxService;
import com.sismics.docs.core.util.ConfigUtil;
import com.sismics.docs.core.util.DirectoryUtil;
import com.sismics.docs.core.util.authentication.LdapAuthenticationHandler;
import com.sismics.docs.core.util.jpa.PaginatedList;
import com.sismics.docs.core.util.jpa.PaginatedLists;
import com.sismics.docs.rest.constant.BaseFunction;
@ -27,12 +26,6 @@ import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.log4j.LogCriteria;
import com.sismics.util.log4j.LogEntry;
import com.sismics.util.log4j.MemoryAppender;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.json.Json;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonObjectBuilder;
@ -40,6 +33,12 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
@ -854,9 +853,6 @@ public class AppResource extends BaseResource {
configDao.update(ConfigType.LDAP_ENABLED, Boolean.FALSE.toString());
}
// Reset the LDAP pool to reconnect with the new configuration
LdapAuthenticationHandler.reset();
return Response.ok().build();
}
}

View File

@ -32,7 +32,6 @@ import com.sismics.docs.core.model.jpa.Document;
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.DocumentUtil;
import com.sismics.docs.core.util.FileUtil;
import com.sismics.docs.core.util.MetadataUtil;
@ -1149,29 +1148,15 @@ public class DocumentResource extends BaseResource {
// Delete the document
documentDao.delete(id, principal.getId());
long totalSize = 0L;
for (File file : fileList) {
// Store the file size to update the quota
java.nio.file.Path storedFile = DirectoryUtil.getStorageDirectory().resolve(file.getId());
try {
totalSize += Files.size(storedFile);
} catch (IOException e) {
// The file doesn't exists on disk, which is weird, but not fatal
}
// Raise file deleted event
FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent();
fileDeletedAsyncEvent.setUserId(principal.getId());
fileDeletedAsyncEvent.setFileId(file.getId());
fileDeletedAsyncEvent.setFileSize(file.getSize());
ThreadLocalContext.get().addAsyncEvent(fileDeletedAsyncEvent);
}
// Update the user quota
UserDao userDao = new UserDao();
User user = userDao.getById(principal.getId());
user.setStorageCurrent(user.getStorageCurrent() - totalSize);
userDao.updateQuota(user);
// Raise a document deleted event
DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent();
documentDeletedAsyncEvent.setUserId(principal.getId());

View File

@ -442,7 +442,7 @@ public class FileResource extends BaseResource {
/**
* List all versions of a file.
*
* @api {get} /file/id/versions Get versions of a file
* @api {get} /file/:id/versions Get versions of a file
* @apiName GetFileVersions
* @apiGroup File
* @apiParam {String} id File ID
@ -522,21 +522,11 @@ public class FileResource extends BaseResource {
FileDao fileDao = new FileDao();
fileDao.delete(file.getId(), principal.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.updateQuota(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.setUserId(principal.getId());
fileDeletedAsyncEvent.setFileId(file.getId());
fileDeletedAsyncEvent.setFileSize(file.getSize());
ThreadLocalContext.get().addAsyncEvent(fileDeletedAsyncEvent);
if (file.getDocumentId() != null) {

View File

@ -470,22 +470,8 @@ public class UserResource extends BaseResource {
UserDao userDao = new UserDao();
userDao.delete(principal.getName(), principal.getId());
// Raise deleted events for documents
for (Document document : documentList) {
DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent();
documentDeletedAsyncEvent.setUserId(principal.getId());
documentDeletedAsyncEvent.setDocumentId(document.getId());
ThreadLocalContext.get().addAsyncEvent(documentDeletedAsyncEvent);
}
// Raise deleted events for files (don't bother sending document updated event)
for (File file : fileList) {
FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent();
fileDeletedAsyncEvent.setUserId(principal.getId());
fileDeletedAsyncEvent.setFileId(file.getId());
ThreadLocalContext.get().addAsyncEvent(fileDeletedAsyncEvent);
}
sendDeletionEvents(documentList, fileList);
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok");
@ -551,23 +537,9 @@ public class UserResource extends BaseResource {
// Delete the user
userDao.delete(user.getUsername(), principal.getId());
// Raise deleted events for documents
for (Document document : documentList) {
DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent();
documentDeletedAsyncEvent.setUserId(principal.getId());
documentDeletedAsyncEvent.setDocumentId(document.getId());
ThreadLocalContext.get().addAsyncEvent(documentDeletedAsyncEvent);
}
// Raise deleted events for files (don't bother sending document updated event)
for (File file : fileList) {
FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent();
fileDeletedAsyncEvent.setUserId(principal.getId());
fileDeletedAsyncEvent.setFileId(file.getId());
ThreadLocalContext.get().addAsyncEvent(fileDeletedAsyncEvent);
}
sendDeletionEvents(documentList, fileList);
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok");
@ -1178,4 +1150,29 @@ public class UserResource extends BaseResource {
}
return null;
}
/**
* Send the events about documents and files being deleted.
* @param documentList A document list
* @param fileList A file list
*/
private void sendDeletionEvents(List<Document> documentList, List<File> fileList) {
// Raise deleted events for documents
for (Document document : documentList) {
DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent();
documentDeletedAsyncEvent.setUserId(principal.getId());
documentDeletedAsyncEvent.setDocumentId(document.getId());
ThreadLocalContext.get().addAsyncEvent(documentDeletedAsyncEvent);
}
// Raise deleted events for files (don't bother sending document updated event)
for (File file : fileList) {
FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent();
fileDeletedAsyncEvent.setUserId(principal.getId());
fileDeletedAsyncEvent.setFileId(file.getId());
fileDeletedAsyncEvent.setFileSize(file.getSize());
ThreadLocalContext.get().addAsyncEvent(fileDeletedAsyncEvent);
}
}
}

View File

@ -429,7 +429,7 @@ angular.module('docs',
prefix: 'locale/',
suffix: '.json?@build.date@'
})
.registerAvailableLanguageKeys(['en', 'es', 'pt', 'fr', 'de', 'el', 'ru', 'it', 'pl', 'zh_CN', 'zh_TW'], {
.registerAvailableLanguageKeys(['en', 'es', 'pt', 'fr', 'de', 'el', 'ru', 'it', 'pl', 'zh_CN', 'zh_TW', 'sq_AL'], {
'en_*': 'en',
'es_*': 'es',
'pt_*': 'pt',
@ -547,7 +547,8 @@ angular.module('docs',
{ key: 'dan', label: 'Dansk' },
{ key: 'nor', label: 'Norsk' },
{ key: 'vie', label: 'Tiếng Việt' },
{ key: 'ces', label: 'Czech' }
{ key: 'ces', label: 'Czech' },
{ key: 'sqi', label: 'Shqip' }
];
})
/**

View File

@ -61,7 +61,7 @@ angular.module('share',
prefix: 'locale/',
suffix: '.json?@build.date@'
})
.registerAvailableLanguageKeys(['en', 'es', 'pt', 'fr', 'de', 'el', 'ru', 'it', 'pl', 'zh_CN', 'zh_TW'], {
.registerAvailableLanguageKeys(['en', 'es', 'pt', 'fr', 'de', 'el', 'ru', 'it', 'pl', 'zh_CN', 'zh_TW', 'sq_AL'], {
'en_*': 'en',
'es_*': 'es',
'pt_*': 'pt',

View File

@ -192,6 +192,7 @@
<span ng-switch-when="pl">Polski</span>
<span ng-switch-when="zh_CN">简体中文</span>
<span ng-switch-when="zh_TW">繁體中文</span>
<span ng-switch-when="sq_AL">Shqip</span>
</span>
<span class="caret"></span>
</a>
@ -207,6 +208,7 @@
<li><a href ng-click="changeLanguage('pl')" ng-class="{ 'bg-info': currentLang == 'pl' }">Polski</a></li>
<li><a href ng-click="changeLanguage('zh_CN')" ng-class="{ 'bg-info': currentLang == 'zh_CN' }">简体中文</a></li>
<li><a href ng-click="changeLanguage('zh_TW')" ng-class="{ 'bg-info': currentLang == 'zh_TW' }">繁體中文</a></li>
<li><a href ng-click="changeLanguage('sq_AL')" ng-class="{ 'bg-info': currentLang == 'sq_AL' }">Shqip</a></li>
</ul>
</li>
<li translate="document.default.footer_sismics"></li>

View File

@ -0,0 +1,150 @@
'use strict';
angular.module("ngLocale", [], ["$provide", function($provide) {
var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
function getDecimals(n) {
n = n + '';
var i = n.indexOf('.');
return (i == -1) ? 0 : n.length - i - 1;
}
function getVF(n, opt_precision) {
var v = opt_precision;
if (undefined === v) {
v = Math.min(getDecimals(n), 3);
}
var base = Math.pow(10, v);
var f = ((n * base) | 0) % base;
return {v: v, f: f};
}
$provide.value("$locale", {
"DATETIME_FORMATS": {
"AMPMS": [
"PD",
"MD"
],
"DAY": [
"E Diel",
"E Hënë",
"E Martë",
"E Mërkurë",
"E Enjte",
"E Premte",
"E Shtunë"
],
"ERANAMES": [
"Para Krishtit",
"Pas Krishtit"
],
"ERAS": [
"p.K.",
"n.K."
],
"FIRSTDAYOFWEEK": 1,
"MONTH": [
"Janar",
"Shkurt",
"Mars",
"Prill",
"Maj",
"Qershor",
"Korrik",
"Gusht",
"Shtator",
"Tetor",
"Nëntor",
"Dhjetor"
],
"SHORTDAY": [
"Die",
"Hën",
"Mar",
"Mër",
"Enj",
"Pre",
"Sht"
],
"SHORTMONTH": [
"Jan",
"Shk",
"Mar",
"Pri",
"Maj",
"Qer",
"Kor",
"Gus",
"Sht",
"Tet",
"Nën",
"Dhj"
],
"STANDALONEMONTH": [
"Janar",
"Shkurt",
"Mars",
"Prill",
"Maj",
"Qershor",
"Korrik",
"Gusht",
"Shtator",
"Tetor",
"Nëntor",
"Dhjetor"
],
"WEEKENDRANGE": [
6,
0
],
"fullDate": "EEEE, d MMMM y",
"longDate": "d MMMM y",
"medium": "d MMM y h:mm:ss a",
"mediumDate": "d MMM y",
"mediumTime": "h:mm:ss a",
"short": "yy-MM-dd h:mm a",
"shortDate": "yy-MM-dd",
"shortTime": "h:mm a"
},
"NUMBER_FORMATS": {
"CURRENCY_SYM": "Lek",
"DECIMAL_SEP": ".",
"GROUP_SEP": ",",
"PATTERNS": [
{
"gSize": 3,
"lgSize": 3,
"maxFrac": 3,
"minFrac": 0,
"minInt": 1,
"negPre": "-",
"negSuf": "",
"posPre": "",
"posSuf": ""
},
{
"gSize": 3,
"lgSize": 3,
"maxFrac": 2,
"minFrac": 2,
"minInt": 1,
"negPre": "-\u00a4",
"negSuf": "",
"posPre": "\u00a4",
"posSuf": ""
}
]
},
"id": "sq-al",
"localeID": "sq_AL",
"pluralCat": function(n, opt_precision) {
var i = n | 0;
var vf = getVF(n, opt_precision);
if (i == 1 && vf.v == 0) {
return PLURAL_CATEGORY.ONE;
}
return PLURAL_CATEGORY.OTHER;
}
});
}]);

View File

@ -0,0 +1,640 @@
{
"login": {
"username": "Emri i përdoruesit",
"password": "Fjalëkalimi",
"validation_code_required": "Kërkohet një kod verifikimi",
"validation_code_title": "Ju keni aktivizuar vërtetimin me dy faktorë në llogarinë tuaj. ",
"validation_code": "Kodi i verifikimit",
"remember_me": "Më kujto mua",
"submit": "Hyni",
"login_as_guest": "Identifikohu si i ftuar",
"login_failed_title": "Identifikimi dështoi",
"login_failed_message": "Emri i përdoruesit ose fjalëkalimi është i pavlefshëm",
"password_lost_btn": "Fjalëkalimi i humbur?",
"password_lost_sent_title": "Email për rivendosjen e fjalëkalimit u dërgua",
"password_lost_sent_message": "Një email është dërguar në <strong>{{ username }}</strong> për të rivendosur fjalëkalimin tuaj",
"password_lost_error_title": "Gabim i rivendosjes së fjalëkalimit",
"password_lost_error_message": "Nuk mund të dërgohet një email për rivendosjen e fjalëkalimit, ju lutemi kontaktoni administratorin tuaj për një rivendosje manuale"
},
"passwordlost": {
"title": "Fjalëkalimi ka humbur",
"message": "Ju lutemi shkruani emrin tuaj të përdoruesit për të marrë një lidhje të rivendosjes së fjalëkalimit. ",
"submit": "Rivendos fjalëkalimin tim"
},
"passwordreset": {
"message": "Ju lutemi shkruani një fjalëkalim të ri",
"submit": "Ndrysho fjalëkalimin tim",
"error_title": "Gabim gjatë ndryshimit të fjalëkalimit tuaj",
"error_message": "Kërkesa juaj për rikuperimin e fjalëkalimit ka skaduar, ju lutemi kërkoni një të re në faqen e hyrjes"
},
"index": {
"toggle_navigation": "Ndrysho navigimin",
"nav_documents": "Dokumentet",
"nav_tags": "Etiketa",
"nav_users_groups": "Përdoruesit",
"error_info": "{{ count }} gabim i ri{{ count > 1 ? 's' : '' }}",
"logged_as": "I identifikuar si {{ username }}",
"nav_settings": "Cilësimet",
"logout": "Shkyç",
"global_quota_warning": "<strong>Paralajmërim!</strong> Kuota globale pothuajse arriti në {{ current | number: 0 }}MB ({{ percent | number: 1 }}%) përdoret në {{ total | number: 0 }}MB"
},
"document": {
"navigation_up": "Ngjitu një nivel",
"toggle_navigation": "Ndrysho navigimin e dosjeve",
"display_mode_list": "Shfaq dokumentet në listë",
"display_mode_grid": "Shfaq dokumentet në rrjet",
"search_simple": "Kërkim i thjeshtë",
"search_fulltext": "Kërkimi i tekstit të plotë",
"search_creator": "Krijuesi",
"search_language": "Gjuhe",
"search_before_date": "Krijuar para kësaj date",
"search_after_date": "Krijuar pas kësaj date",
"search_before_update_date": "Përditësuar përpara kësaj date",
"search_after_update_date": "Përditësuar pas kësaj date",
"search_tags": "Etiketa",
"search_shared": "Vetëm dokumente të përbashkëta",
"search_workflow": "Rrjedha e punës më është caktuar",
"search_clear": "Qartë",
"any_language": "Çdo gjuhë",
"add_document": "Shto një dokument",
"import_eml": "Importo nga një email (format EML)",
"tags": "Etiketa",
"no_tags": "Nuk ka etiketa",
"no_documents": "Asnjë dokument në bazën e të dhënave",
"search": "Kërko",
"search_empty": "Nuk ka ndeshje për <strong>\"{{ search }}\"</strong>",
"shared": "Të përbashkëta",
"current_step_name": "Hapi aktual",
"title": "Titulli",
"description": "Përshkrim",
"contributors": "Kontribuesit",
"language": "Gjuhe",
"creation_date": "Data e krijimit",
"subject": "Subjekti",
"identifier": "Identifikues",
"publisher": "Botues",
"format": "Formati",
"source": "Burimi",
"type": "Lloji",
"coverage": "Mbulimi",
"rights": "Të drejtat",
"relations": "Marrëdhëniet",
"page_size": "Madhësia e faqes",
"page_size_10": "10 për faqe",
"page_size_20": "20 për faqe",
"page_size_30": "30 për faqe",
"upgrade_quota": "Për të përmirësuar kuotën tuaj, pyesni administratorin tuaj",
"quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) përdoret në {{ total | number: 0 }}MB",
"count": "{{ count }} dokument{{ count > 1 ? 's' : '' }} gjetur",
"last_updated": "Përditësimi i fundit {{ date | timeAgo: dateFormat }}",
"view": {
"delete_comment_title": "Fshi komentin",
"delete_comment_message": "Dëshiron vërtet ta fshish këtë koment?",
"delete_document_title": "Fshi dokumentin",
"delete_document_message": "Dëshiron vërtet ta fshish këtë dokument?",
"shared_document_title": "Dokument i përbashkët",
"shared_document_message": "Ju mund ta ndani këtë dokument duke dhënë këtë lidhje. <br/><input class=\"form-control share-link\" type=\"text\" readonly=\"readonly\" value=\"{{ link }}\" onclick=\"this.select(); document.execCommand('copy');\" />",
"not_found": "Dokumenti nuk u gjet",
"forbidden": "Qasja është e ndaluar",
"download_files": "Shkarko skedarët",
"export_pdf": "Eksporto në PDF",
"by_creator": "nga",
"comments": "Komentet",
"no_comments": "Ende nuk ka komente për këtë dokument",
"add_comment": "Shto një koment",
"error_loading_comments": "Gabim gjatë ngarkimit të komenteve",
"workflow_current": "Hapi aktual i rrjedhës së punës",
"workflow_comment": "Shto një koment të rrjedhës së punës",
"workflow_validated_title": "Hapi i rrjedhës së punës u vërtetua",
"workflow_validated_message": "Hapi i rrjedhës së punës është vërtetuar me sukses.",
"content": {
"content": "përmbajtja",
"delete_file_title": "Fshi skedarin",
"delete_file_message": "Dëshiron vërtet ta fshish këtë skedar?",
"upload_pending": "Në pritje...",
"upload_progress": "Po ngarkohet...",
"upload_error": "Gabim ngarkimi",
"upload_error_quota": "Kuota u arrit",
"drop_zone": "Zvarrit",
"add_files": "Shtoni skedarë",
"file_processing_indicator": "Ky skedar është duke u përpunuar. ",
"reprocess_file": "Ripërpunoni këtë skedar",
"upload_new_version": "Ngarko një version të ri",
"open_versions": "Shfaq historikun e versionit",
"display_mode_list": "Shfaq skedarët në listë",
"display_mode_grid": "Shfaq skedarët në rrjet"
},
"workflow": {
"workflow": "Rrjedha e punës",
"message": "Verifikoni ose vërtetoni dokumentet tuaja me njerëzit e organizatës suaj duke përdorur rrjedhat e punës.",
"workflow_start_label": "Cilin rrjedhë pune të filloni?",
"add_more_workflow": "Shto më shumë flukse pune",
"start_workflow_submit": "Filloni rrjedhën e punës",
"full_name": "<strong>{{ name }}</strong> filloi më {{ create_date | date }}",
"cancel_workflow": "Anuloni rrjedhën aktuale të punës",
"cancel_workflow_title": "Anuloni rrjedhën e punës",
"cancel_workflow_message": "Dëshiron vërtet të anulosh rrjedhën aktuale të punës?",
"no_workflow": "Nuk mund të filloni asnjë rrjedhë pune në këtë dokument."
},
"permissions": {
"permissions": "Lejet",
"message": "Lejet mund të aplikohen drejtpërdrejt në këtë dokument, ose mund të vijnë nga <a href=\"#/tag\">etiketa</a>.",
"title": "Lejet për këtë dokument",
"inherited_tags": "Lejet e trashëguara nga etiketat",
"acl_source": "Nga",
"acl_target": "Për",
"acl_permission": "Leja"
},
"activity": {
"activity": "Aktiviteti",
"message": "Çdo veprim në këtë dokument regjistrohet këtu."
}
},
"edit": {
"document_edited_with_errors": "Dokumenti u redaktua me sukses, por disa skedarë nuk mund të ngarkohen",
"document_added_with_errors": "Dokumenti u shtua me sukses, por disa skedarë nuk mund të ngarkohen",
"quota_reached": "Kuota u arrit",
"primary_metadata": "Meta të dhënat primare",
"title_placeholder": "Një emër i dhënë burimit",
"description_placeholder": "Një llogari e burimit",
"new_files": "Skedarë të rinj",
"orphan_files": "{{ count }} dosje{{ count > 1 ? 's' : '' }}",
"additional_metadata": "Meta të dhëna shtesë",
"subject_placeholder": "Tema e burimit",
"identifier_placeholder": "Një referencë e paqartë për burimin brenda një konteksti të caktuar",
"publisher_placeholder": "Një subjekt përgjegjës për vënien në dispozicion të burimit",
"format_placeholder": "Formati i skedarit, mediumi fizik ose dimensionet e burimit",
"source_placeholder": "Një burim i lidhur nga i cili rrjedh burimi i përshkruar",
"uploading_files": "Skedarët po ngarkohen..."
},
"default": {
"upload_pending": "Në pritje...",
"upload_progress": "Po ngarkohet...",
"upload_error": "Gabim ngarkimi",
"upload_error_quota": "Kuota u arrit",
"quick_upload": "Ngarkimi i shpejtë",
"drop_zone": "Zvarrit",
"add_files": "Shtoni skedarë",
"add_new_document": "Shto në dokument të ri",
"latest_activity": "Aktiviteti i fundit",
"footer_sismics": "E punuar me <span class=\"fas fa-heart\"></span> nga <a href=\"https://www.sismics.com\" target=\"_blank\">Sizmike</a>",
"api_documentation": "Dokumentacioni API",
"feedback": "Na jepni një koment",
"workflow_document_list": "Dokumentet e caktuara për ju",
"select_all": "Selektoj të gjitha",
"select_none": "Zgjidh asnjë"
},
"pdf": {
"export_title": "Eksporto në PDF",
"export_metadata": "Eksporto të dhëna meta",
"export_comments": "Eksporto komente",
"fit_to_page": "Përshtat imazhin në faqe",
"margin": "Marzhi",
"millimeter": "mm"
},
"share": {
"title": "Ndani dokumentin",
"message": "Emërtoni ndarjen nëse dëshironi të ndani disa herë të njëjtin dokument.",
"submit": "Shpërndaje"
}
},
"file": {
"view": {
"previous": "E mëparshme",
"next": "Tjetra",
"not_found": "Skedari nuk u gjet"
},
"edit": {
"title": "Redakto skedarin",
"name": "Emri i skedarit"
},
"versions": {
"title": "Historia e versionit",
"filename": "Emri i skedarit",
"mimetype": "Lloji",
"create_date": "Data e krijimit",
"version": "Version"
}
},
"tag": {
"new_tag": "Etiketë e re",
"search": "Kërko",
"default": {
"title": "Etiketa",
"message_1": "<strong>Etiketa</strong> janë etiketa të lidhura me dokumentet.",
"message_2": "Një dokument mund të etiketohet me etiketa të shumta dhe një etiketë mund të aplikohet në dokumente të shumta.",
"message_3": "Duke perdorur <span class=\"fas fa-pencil-alt\"></span> butonin, ju mund të modifikoni lejet në një etiketë.",
"message_4": "Nëse një etiketë mund të lexohet nga një përdorues ose grup tjetër, dokumentet shoqëruese mund të lexohen gjithashtu nga ata njerëz.",
"message_5": "Për shembull, etiketoni dokumentet e kompanisë suaj me një etiketë <span class=\"label label-info\">Kompania ime</span> dhe shtoni lejen <strong>Mund të lexojë</strong> në një grup <span class=\"btn btn-default\">punonjësit</span>"
},
"edit": {
"delete_tag_title": "Fshi etiketën",
"delete_tag_message": "Dëshiron vërtet ta fshish këtë etiketë?",
"name": "Emri",
"color": "Ngjyrë",
"parent": "Prindi",
"info": "Lejet për këtë etiketë do të zbatohen gjithashtu për dokumentet e etiketuara <span class=\"label label-info\" ng-style=\"{ 'background': color }\">{{ name }}</span>",
"circular_reference_title": "Referencë rrethore",
"circular_reference_message": "Hierarkia e etiketave prind krijon një lak, ju lutemi zgjidhni një prind tjetër."
}
},
"group": {
"profile": {
"members": "Anëtarët",
"no_members": "Asnjë anëtar",
"related_links": "Lidhje të ngjashme",
"edit_group": "Redakto {{ name }} grup"
}
},
"user": {
"profile": {
"groups": "Grupet",
"quota_used": "Kuota e përdorur",
"percent_used": "{{ percent | number: 0 }}% e përdorur",
"related_links": "Lidhje të ngjashme",
"document_created": "Dokumentet e krijuara nga {{ username }}",
"edit_user": "Redakto {{ username }} përdorues"
}
},
"usergroup": {
"search_groups": "Kërkoni në grupe",
"search_users": "Kërkoni në përdoruesit",
"you": "je ti!",
"default": {
"title": "Përdoruesit",
"message": "Këtu mund të shikoni informacione rreth përdoruesve dhe grupeve."
}
},
"settings": {
"menu_personal_settings": "Cilësimet personale",
"menu_user_account": "Llogaria e përdoruesit",
"menu_two_factor_auth": "Autentifikimi me dy faktorë",
"menu_opened_sessions": "Seancat e hapura",
"menu_file_importer": "Importuesi i skedarëve në masë",
"menu_general_settings": "Cilësimet e përgjithshme",
"menu_workflow": "Rrjedha e punës",
"menu_users": "Përdoruesit",
"menu_groups": "Grupet",
"menu_vocabularies": "Fjalorët",
"menu_configuration": "Konfigurimi",
"menu_inbox": "Skanimi i kutisë hyrëse",
"menu_ldap": "Autentifikimi LDAP",
"menu_metadata": "Meta të dhëna të personalizuara",
"menu_monitoring": "Monitorimi",
"ldap": {
"title": "Autentifikimi LDAP",
"enabled": "Aktivizo vërtetimin LDAP",
"host": "Emri i hostit LDAP",
"port": "Porta LDAP (389 si parazgjedhje)",
"usessl": "Aktivizo SSL (ldaps)",
"admin_dn": "Admin DN",
"admin_password": "Fjalëkalimi i administratorit",
"base_dn": "Kërkimi bazë DN",
"filter": "Filtri i kërkimit (duhet të përmbajë USERNAME, p.sh. \"(uid=USERNAME)\")",
"default_email": "Email-i i parazgjedhur për përdoruesin LDAP",
"default_storage": "Hapësira ruajtëse e paracaktuar për përdoruesin LDAP",
"saved": "Konfigurimi LDAP u ruajt me sukses"
},
"user": {
"title": "Menaxhimi i përdoruesve",
"add_user": "Shto një përdorues",
"username": "Emri i përdoruesit",
"create_date": "Krijo datë",
"totp_enabled": "Për këtë llogari është aktivizuar vërtetimi me dy faktorë",
"edit": {
"delete_user_title": "Fshi përdoruesin",
"delete_user_message": "Dëshiron vërtet ta fshish këtë përdorues? ",
"user_used_title": "Përdoruesi në përdorim",
"user_used_message": "Ky përdorues përdoret në rrjedhën e punës \"{{ name }}\"",
"edit_user_failed_title": "Përdoruesi ekziston tashmë",
"edit_user_failed_message": "Ky emër përdoruesi është marrë tashmë nga një përdorues tjetër",
"edit_user_title": "Redakto \"{{ username }}\"",
"add_user_title": "Shto një përdorues",
"username": "Emri i përdoruesit",
"email": "E-mail",
"groups": "Grupet",
"storage_quota": "Kuota e ruajtjes",
"storage_quota_placeholder": "Kuota e hapësirës ruajtëse (në MB)",
"password": "Fjalëkalimi",
"password_confirm": "Fjalëkalimi (konfirmo)",
"disabled": "Përdorues me aftësi të kufizuara",
"password_reset_btn": "Dërgoni një email për rivendosjen e fjalëkalimit te ky përdorues",
"password_lost_sent_title": "Email për rivendosjen e fjalëkalimit u dërgua",
"password_lost_sent_message": "Është dërguar një email për rivendosjen e fjalëkalimit <strong>{{ username }}</strong>",
"disable_totp_btn": "Çaktivizo vërtetimin me dy faktorë për këtë përdorues",
"disable_totp_title": "Çaktivizo vërtetimin me dy faktorë",
"disable_totp_message": "Jeni i sigurt që dëshironi të çaktivizoni vërtetimin me dy faktorë për këtë përdorues?"
}
},
"workflow": {
"title": "Konfigurimi i rrjedhës së punës",
"add_workflow": "Shto një rrjedhë pune",
"name": "Emri",
"create_date": "Krijo datë",
"edit": {
"delete_workflow_title": "Fshi fluksin e punës",
"delete_workflow_message": "Dëshiron vërtet ta fshish këtë rrjedhë pune? ",
"edit_workflow_title": "Redakto \"{{ name }}\"",
"add_workflow_title": "Shto një rrjedhë pune",
"name": "Emri",
"name_placeholder": "Emri ose përshkrimi i hapit",
"drag_help": "Zvarrit dhe lësho për të rirenditur hapin",
"type": "Lloji i hapit",
"type_approve": "Mirato",
"type_validate": "Vërtetoni",
"target": "Caktuar për",
"target_help": "<strong>Mirato:</strong> Pranoni ose refuzoni rishikimin<br/><strong>Vërteto:</strong> Rishikoni dhe vazhdoni rrjedhën e punës",
"add_step": "Shto një hap të rrjedhës së punës",
"actions": "Çfarë ndodh më pas?",
"remove_action": "Hiq veprimin",
"acl_info": "Vetëm përdoruesit dhe grupet e përcaktuara këtu do të mund të fillojnë këtë rrjedhë pune në një dokument"
}
},
"security": {
"enable_totp": "Aktivizo vërtetimin me dy faktorë",
"enable_totp_message": "Sigurohuni që të keni një aplikacion të përputhshëm me TOTP në telefonin tuaj gati për të shtuar një llogari të re",
"title": "Autentifikimi me dy faktorë",
"message_1": "Autentifikimi me dy faktorë ju lejon të shtoni një shtresë sigurie në tuaj <strong>{{ appName }}</strong> llogari.<br/>Përpara se të aktivizoni këtë veçori, sigurohuni që të keni një aplikacion të pajtueshëm me TOTP në telefonin tuaj:",
"message_google_authenticator": "Për Android, iOS dhe Blackberry: <a href=\"https://support.google.com/accounts/answer/1066447\" target=\"_blank\">Google Authenticator</a>",
"message_duo_mobile": "Për Android dhe iOS: <a href=\"https://guide.duo.com/third-party-accounts\" target=\"_blank\">Duo Mobile</a>",
"message_authenticator": "Për Windows Phone: <a href=\"https://www.microsoft.com/en-US/store/apps/Authenticator/9WZDNCRFJ3RJ\" target=\"_blank\">Vërtetuesi</a>",
"message_2": "Këto aplikacione gjenerojnë automatikisht një kod verifikimi që ndryshon pas një periudhe të caktuar kohe.<br/>Do t'ju kërkohet të vendosni këtë kod verifikimi sa herë që identifikoheni <strong>{{ appName }}</strong>.",
"secret_key": "Çelësi juaj sekret është: <strong>{{ secret }}</strong>",
"secret_key_warning": "Konfiguro aplikacionin tënd TOTP në telefonin tënd me këtë çelës sekret tani, nuk do të mund ta qasesh më vonë.",
"totp_enabled_message": "Autentifikimi me dy faktorë është aktivizuar në llogarinë tuaj.<br/>Sa herë që identifikoheni <strong>{{ appName }}</strong>, do t'ju kërkohet një kod verifikimi nga aplikacioni i telefonit tuaj të konfiguruar.<br/>Nëse e humbni telefonin, nuk do të jeni në gjendje të identifikoheni në llogarinë tuaj, por seancat aktive do t'ju lejojnë të rigjeneroni një çelës sekret.",
"disable_totp": {
"disable_totp": "Çaktivizo vërtetimin me dy faktorë",
"message": "Llogaria juaj nuk do të mbrohet më nga vërtetimi me dy faktorë.",
"confirm_password": "Konfirmoni fjalëkalimin tuaj",
"submit": "Çaktivizo vërtetimin me dy faktorë"
},
"test_totp": "Ju lutemi shkruani kodin e vërtetimit të shfaqur në telefonin tuaj:",
"test_code_success": "Kodi i verifikimit në rregull",
"test_code_fail": "Ky kod nuk është i vlefshëm, ju lutemi kontrolloni dy herë nëse telefoni juaj është i konfiguruar siç duhet ose çaktivizoni vërtetimin me dy faktorë"
},
"group": {
"title": "Menaxhimi i grupeve",
"add_group": "Shto një grup",
"name": "Emri",
"edit": {
"delete_group_title": "Fshi grupin",
"delete_group_message": "Dëshiron vërtet ta fshish këtë grup?",
"edit_group_failed_title": "Grupi tashmë ekziston",
"edit_group_failed_message": "Ky emër grupi është marrë tashmë nga një grup tjetër",
"group_used_title": "Grupi në përdorim",
"group_used_message": "Ky grup përdoret në rrjedhën e punës \"{{ name }}\"",
"edit_group_title": "Redakto \"{{ name }}\"",
"add_group_title": "Shto një grup",
"name": "Emri",
"parent_group": "Grupi i prindërve",
"search_group": "Kërkoni një grup",
"members": "Anëtarët",
"new_member": "Anëtar i ri",
"search_user": "Kërkoni një përdorues"
}
},
"account": {
"title": "Llogaria e përdoruesit",
"password": "Fjalëkalimi",
"password_confirm": "Fjalëkalimi (konfirmo)",
"updated": "Llogaria u përditësua me sukses"
},
"config": {
"title_guest_access": "Qasja e mysafirëve",
"message_guest_access": "Qasja e mysafirëve është një mënyrë ku çdokush mund të hyjë {{ appName }} pa fjalëkalim.<br/>Ashtu si një përdorues normal, përdoruesi mysafir mund të qaset vetëm në dokumentet e tij dhe ato të aksesueshme përmes lejeve.<br/>",
"enable_guest_access": "Aktivizo qasjen e vizitorëve",
"disable_guest_access": "Çaktivizo qasjen e vizitorëve",
"title_theme": "Personalizimi i temës",
"title_general": "Konfigurimi i përgjithshëm",
"default_language": "Gjuha e parazgjedhur për dokumentet e reja",
"application_name": "Emri i aplikacionit",
"main_color": "Ngjyra kryesore",
"custom_css": "CSS e personalizuar",
"custom_css_placeholder": "CSS e personalizuar për t'u shtuar pas fletës kryesore të stilit",
"logo": "Logo (madhësia katrore)",
"background_image": "Imazhi i sfondit",
"uploading_image": "Po ngarkon imazhin...",
"title_smtp": "Konfigurimi i emailit",
"smtp_hostname": "Emri i hostit SMTP",
"smtp_port": "Porta SMTP",
"smtp_from": "E-mail i dërguesit",
"smtp_username": "Emri i përdoruesit SMTP",
"smtp_password": "Fjalëkalimi SMTP",
"smtp_updated": "Konfigurimi SMTP u përditësua me sukses",
"webhooks": "Uebhooks",
"webhooks_explain": "Webhooks do të thirren kur të ndodhë ngjarja e specifikuar. ",
"webhook_event": "Ngjarja",
"webhook_url": "URL",
"webhook_create_date": "Krijo datë",
"webhook_add": "Shto një uebhook"
},
"metadata": {
"title": "Konfigurimi i personalizuar i meta të dhënave",
"message": "Këtu mund të shtoni meta të dhëna të personalizuara në dokumentet tuaja si një identifikues i brendshëm ose një datë skadimi. ",
"name": "Emri i meta të dhënave",
"type": "Lloji i meta të dhënave"
},
"inbox": {
"title": "Skanimi i kutisë hyrëse",
"message": "Duke aktivizuar këtë veçori, sistemi do të skanojë kutinë hyrëse të specifikuar çdo minutë <strong>i palexuar</strong> emailet dhe i importoni automatikisht.<br/>Pas importimit të një emaili, ai do të shënohet si i lexuar.<br/>Cilësimet e konfigurimit për <a href=\"https://support.google.com/mail/answer/7126229?hl=en\" target=\"_blank\">Gmail</a>, <a href=\"https://support.office.com/en-us/article/pop-imap-and-smtp-settings-for-outlook-com-d088b986-291d-42b8-9564-9c414e2aa040\" target=\"_blank\">Outlook.com</a>, <a href=\"https://help.yahoo.com/kb/SLN4075.html\" target=\"_blank\">Yahoo</a>.",
"enabled": "Aktivizo skanimin e kutisë hyrëse",
"hostname": "Emri i hostit IMAP",
"port": "Porta IMAP (143 ose 993)",
"starttls": "Aktivizo STARTTLS",
"username": "Emri i përdoruesit IMAP",
"password": "Fjalëkalimi IMAP",
"folder": "Dosja IMAP",
"tag": "Etiketa u shtua në dokumentet e importuara",
"test": "Testoni parametrat",
"last_sync": "Sinkronizimi i fundit: {{ data.date | date: 'medium' }}, {{ data.count }} mesazh{{ data.count > 1 ? 's' : '' }} të importuara",
"test_success": "Lidhja me kutinë hyrëse është e suksesshme ({{ count }} <strong>i palexuar</strong> mesazh{{ count > 1 ? 's' : '' }})",
"test_fail": "Ndodhi një gabim gjatë lidhjes me kutinë hyrëse, ju lutemi kontrolloni parametrat",
"saved": "Konfigurimi IMAP u ruajt me sukses",
"autoTagsEnabled": "Shtoni automatikisht etiketat nga rreshti i subjektit të shënuar me",
"deleteImported": "Fshi mesazhin nga kutia postare pas importimit"
},
"monitoring": {
"background_tasks": "Detyrat e sfondit",
"queued_tasks": "Aktualisht ka {{ count }} detyrat në radhë.",
"queued_tasks_explain": "Përpunimi i skedarëve, krijimi i miniaturave, përditësimi i indeksit, njohja optike e karaktereve janë detyra në sfond. ",
"server_logs": "Regjistrat e serverit",
"log_date": "Data",
"log_tag": "Etiketë",
"log_message": "Mesazh",
"indexing": "Indeksimi",
"indexing_info": "Nëse vëreni mospërputhje në rezultatet e kërkimit, mund të provoni të bëni një riindeksim të plotë. ",
"start_reindexing": "Filloni riindeksimin e plotë",
"reindexing_started": "Ri-indeksimi filloi, ju lutemi prisni derisa të mos ketë më detyra në sfond."
},
"session": {
"title": "Seancat e hapura",
"created_date": "Data e krijimit",
"last_connection_date": "Data e fundit e lidhjes",
"user_agent": "Nga",
"current": "Aktuale",
"current_session": "Ky është sesioni aktual",
"clear_message": "Të gjitha pajisjet e tjera të lidhura me këtë llogari do të shkëputen",
"clear": "Pastro të gjitha seancat e tjera"
},
"vocabulary": {
"title": "Shënimet e fjalorit",
"choose_vocabulary": "Zgjidhni një fjalor për të redaktuar",
"type": "Lloji",
"coverage": "Mbulimi",
"rights": "Të drejtat",
"value": "Vlera",
"order": "Rendit",
"new_entry": "Hyrje e re"
},
"fileimporter": {
"title": "Importuesi i skedarëve në masë",
"advanced_users": "Për përdoruesit e avancuar!",
"need_intro": "Nëse ju duhet:",
"need_1": "Importoni një direktori skedarësh menjëherë",
"need_2": "Skanoni një drejtori për skedarë të rinj dhe importojini ato",
"line_1": "Shkoni në <a href=\"https://github.com/sismics/docs/releases\">sismics/docs/releases</a> dhe shkarkoni mjetin e importuesit të skedarëve për sistemin tuaj.",
"line_2": "Ndiq <a href=\"https://github.com/sismics/docs/tree/master/docs-importer\">udhëzime këtu</a> për të përdorur këtë mjet.",
"line_3": "Skedarët tuaj do të importohen në dokumente sipas konfigurimit të importuesit të skedarëve.",
"download": "Shkarko",
"instructions": "Udhëzimet"
}
},
"feedback": {
"title": "Na jepni një koment",
"message": "Ndonjë sugjerim apo pyetje në lidhje me Teedy? ",
"sent_title": "Komentet u dërguan",
"sent_message": "Faleminderit për komentin tuaj! "
},
"import": {
"title": "Importimi",
"error_quota": "U arrit kufiri i kuotës, kontaktoni administratorin tuaj për të rritur kuotën tuaj",
"error_general": "Ndodhi një gabim gjatë përpjekjes për të importuar skedarin tuaj, ju lutemi sigurohuni që ai është një skedar i vlefshëm EML"
},
"app_share": {
"403": {
"title": "I pa autorizuar",
"message": "Dokumenti që po përpiqeni të shikoni nuk ndahet më"
},
"main": "Kërkoni një lidhje të përbashkët të dokumentit për të hyrë në të"
},
"directive": {
"acledit": {
"acl_target": "Për",
"acl_permission": "Leja",
"add_permission": "Shto një leje",
"search_user_group": "Kërkoni një përdorues ose grup"
},
"auditlog": {
"log_created": "krijuar",
"log_updated": "përditësuar",
"log_deleted": "fshihet",
"Acl": "ACL",
"Comment": "Komentoni",
"Document": "Dokumenti",
"File": "Skedari",
"Group": "Grupi",
"Route": "Rrjedha e punës",
"RouteModel": "Modeli i rrjedhës së punës",
"Tag": "Etiketë",
"User": "Përdoruesi",
"Webhook": "Uebhook"
},
"selectrelation": {
"typeahead": "Shkruani një titull dokumenti"
},
"selecttag": {
"typeahead": "Shkruani një etiketë"
},
"datepicker": {
"current": "Sot",
"clear": "Qartë",
"close": "U krye"
}
},
"filter": {
"filesize": {
"mb": "MB",
"kb": "kB"
}
},
"acl": {
"READ": "Mund të lexojë",
"READWRITE": "Mund të shkruajë",
"WRITE": "Mund të shkruajë",
"USER": "Përdoruesi",
"GROUP": "Grupi",
"SHARE": "Të përbashkëta"
},
"workflow_type": {
"VALIDATE": "Vleresimi",
"APPROVE": "Miratimi"
},
"workflow_transition": {
"APPROVED": "Miratuar",
"REJECTED": "Refuzuar",
"VALIDATED": "E vërtetuar"
},
"validation": {
"required": "E detyrueshme",
"too_short": "Shumë e shkurtër",
"too_long": "Shume gjate",
"email": "Duhet të jetë një e-mail i vlefshëm",
"password_confirm": "Fjalëkalimi dhe konfirmimi i fjalëkalimit duhet të përputhen",
"number": "Numri i kërkuar",
"no_space": "Hapësirat dhe dy pikat nuk lejohen",
"alphanumeric": "Lejohen vetëm shkronja dhe numra"
},
"action_type": {
"ADD_TAG": "Shto një etiketë",
"REMOVE_TAG": "Hiq një etiketë",
"PROCESS_FILES": "Përpunoni skedarët"
},
"pagination": {
"previous": "E mëparshme",
"next": "Tjetra",
"first": "Së pari",
"last": "E fundit"
},
"onboarding": {
"step1": {
"title": "Hera e parë?",
"description": "Nëse është hera juaj e parë në Teedy, klikoni butonin Next, përndryshe mos ngurroni të më mbyllni."
},
"step2": {
"title": "Dokumentet",
"description": "Teedy është i organizuar në dokumente dhe çdo dokument përmban skedarë të shumtë."
},
"step3": {
"title": "Skedarët",
"description": "Mund të shtoni skedarë pas krijimit të një dokumenti ose përpara se të përdorni këtë zonë të ngarkimit të shpejtë."
},
"step4": {
"title": "Kërko",
"description": "Kjo është mënyra kryesore për të gjetur përsëri dokumentet tuaja. "
},
"step5": {
"title": "Etiketa",
"description": "Dokumentet mund të organizohen në etiketa (të cilat janë si super-dosje). "
}
},
"yes": "po",
"no": "Nr",
"ok": "Në rregull",
"cancel": "Anulo",
"share": "Shpërndaje",
"unshare": "Shpërndaje",
"close": "Mbylle",
"add": "Shtoni",
"open": "Hapur",
"see": "Shiko",
"save": "Ruaj",
"export": "Eksporto",
"edit": "Redakto",
"delete": "Fshije",
"rename": "Riemërto",
"download": "Shkarko",
"loading": "Po ngarkohet...",
"send": "Dërgo",
"enabled": "Aktivizuar",
"disabled": "I paaftë"
}

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="renderer" content="webkit" />
<link rel="shortcut icon" href="favicon.png" />
<link rel="shortcut icon" href="../api/theme/image/logo" />
<!-- ref:css style/style.min.css?@build.date@ -->
<link rel="stylesheet" href="style/bootstrap.css" type="text/css" />
<link rel="stylesheet" href="style/fontawesome.css" type="text/css" />
@ -102,4 +102,4 @@
</div>
</div>
</body>
</html>
</html>

View File

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

View File

@ -105,7 +105,7 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertEquals("PIA00452.jpg", files.getJsonObject(0).getString("name"));
Assert.assertEquals("image/jpeg", files.getJsonObject(0).getString("mimetype"));
Assert.assertEquals(0, files.getJsonObject(0).getInt("version"));
Assert.assertEquals(163510L, files.getJsonObject(0).getJsonNumber("size").longValue());
Assert.assertEquals(FILE_PIA_00452_JPG_SIZE, files.getJsonObject(0).getJsonNumber("size").longValue());
Assert.assertEquals(file2Id, files.getJsonObject(1).getString("id"));
Assert.assertEquals("PIA00452.jpg", files.getJsonObject(1).getString("name"));
Assert.assertEquals(0, files.getJsonObject(1).getInt("version"));
@ -370,7 +370,7 @@ public class TestFileResource extends BaseJerseyTest {
.get();
is = (InputStream) response.getEntity();
fileBytes = ByteStreams.toByteArray(is);
Assert.assertEquals(163510, fileBytes.length);
Assert.assertEquals(FILE_PIA_00452_JPG_SIZE, fileBytes.length);
// Create another document
String document2Id = clientUtil.createDocument(fileOrphanToken);
@ -415,28 +415,19 @@ public class TestFileResource extends BaseJerseyTest {
String file1Id = clientUtil.addFileToDocument(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG, fileQuotaToken, null);
// Check current quota
JsonObject json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken)
.get(JsonObject.class);
Assert.assertEquals(292641L, json.getJsonNumber("storage_current").longValue());
Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE, getUserQuota(fileQuotaToken));
// Add a file (292641 bytes large)
clientUtil.addFileToDocument(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG, fileQuotaToken, null);
// Check current quota
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken)
.get(JsonObject.class);
Assert.assertEquals(585282L, json.getJsonNumber("storage_current").longValue());
Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE * 2, getUserQuota(fileQuotaToken));
// Add a file (292641 bytes large)
clientUtil.addFileToDocument(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG, fileQuotaToken, null);
// Check current quota
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken)
.get(JsonObject.class);
Assert.assertEquals(877923L, json.getJsonNumber("storage_current").longValue());
Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE * 3, getUserQuota(fileQuotaToken));
// Add a file (292641 bytes large)
try {
@ -446,16 +437,13 @@ public class TestFileResource extends BaseJerseyTest {
}
// Deletes a file
json = target().path("/file/" + file1Id).request()
JsonObject json = target().path("/file/" + file1Id).request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken)
.delete(JsonObject.class);
Assert.assertEquals("ok", json.getString("status"));
// Check current quota
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken)
.get(JsonObject.class);
Assert.assertEquals(585282L, json.getJsonNumber("storage_current").longValue());
Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE * 2, getUserQuota(fileQuotaToken));
// Create a document
long create1Date = new Date().getTime();
@ -472,10 +460,7 @@ public class TestFileResource extends BaseJerseyTest {
clientUtil.addFileToDocument(FILE_PIA_00452_JPG, fileQuotaToken, document1Id);
// Check current quota
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken)
.get(JsonObject.class);
Assert.assertEquals(748792, json.getJsonNumber("storage_current").longValue());
Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE * 2 + FILE_PIA_00452_JPG_SIZE, getUserQuota(fileQuotaToken));
// Deletes the document
json = target().path("/document/" + document1Id).request()
@ -484,9 +469,12 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertEquals("ok", json.getString("status"));
// Check current quota
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken)
.get(JsonObject.class);
Assert.assertEquals(585282L, json.getJsonNumber("storage_current").longValue());
Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE * 2, getUserQuota(fileQuotaToken));
}
private long getUserQuota(String userToken) {
return target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, userToken)
.get(JsonObject.class).getJsonNumber("storage_current").longValue();
}
}

View File

@ -46,7 +46,7 @@
<com.icegreen.greenmail.version>1.6.14</com.icegreen.greenmail.version>
<org.jsoup.jsoup.version>1.15.4</org.jsoup.jsoup.version>
<com.squareup.okhttp3.okhttp.version>4.10.0</com.squareup.okhttp3.okhttp.version>
<org.apache.directory.api.api-all.version>2.1.2</org.apache.directory.api.api-all.version>
<org.apache.directory.api.api-all.version>2.1.3</org.apache.directory.api.api-all.version>
<org.glassfish.jersey.version>3.0.10</org.glassfish.jersey.version>
<jakarta.servlet.jakarta.servlet-api.version>5.0.0</jakarta.servlet.jakarta.servlet-api.version>