diff --git a/config/alfresco/subsystems/fileServers/default/file-servers-context.xml b/config/alfresco/subsystems/fileServers/default/file-servers-context.xml index 993197f1d3..7f327c9828 100644 --- a/config/alfresco/subsystems/fileServers/default/file-servers-context.xml +++ b/config/alfresco/subsystems/fileServers/default/file-servers-context.xml @@ -298,6 +298,13 @@ + + + + + + + @@ -324,6 +331,11 @@ true + + + + + diff --git a/source/java/org/alfresco/filesys/repo/ContentContext.java b/source/java/org/alfresco/filesys/repo/ContentContext.java index 7e8d9e0339..a946802437 100644 --- a/source/java/org/alfresco/filesys/repo/ContentContext.java +++ b/source/java/org/alfresco/filesys/repo/ContentContext.java @@ -28,6 +28,7 @@ import org.alfresco.jlan.server.filesys.DiskInterface; import org.alfresco.jlan.server.filesys.DiskSharedDevice; import org.alfresco.jlan.server.filesys.FileName; import org.alfresco.jlan.server.filesys.FileSystem; +import org.alfresco.jlan.server.filesys.quota.QuotaManagerException; import org.alfresco.service.cmr.repository.NodeRef; /** @@ -260,6 +261,16 @@ public class ContentContext extends AlfrescoContext if ( m_nodeMonitor != null) m_nodeMonitor.shutdownRequest(); + // Stop the quota manager, if enabled + + if ( hasQuotaManager()) { + try { + getQuotaManager().stopManager(null, this); + } + catch ( QuotaManagerException ex) { + } + } + // Call the base class super.CloseContext(); diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java index 1a790c0b72..475c4cde33 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java @@ -53,11 +53,14 @@ import org.alfresco.jlan.server.filesys.FileStatus; import org.alfresco.jlan.server.filesys.NetworkFile; import org.alfresco.jlan.server.filesys.SearchContext; import org.alfresco.jlan.server.filesys.TreeConnection; +import org.alfresco.jlan.server.filesys.db.DBFileInfo; import org.alfresco.jlan.server.filesys.pseudo.MemoryNetworkFile; import org.alfresco.jlan.server.filesys.pseudo.PseudoFile; import org.alfresco.jlan.server.filesys.pseudo.PseudoFileInterface; import org.alfresco.jlan.server.filesys.pseudo.PseudoFileList; import org.alfresco.jlan.server.filesys.pseudo.PseudoNetworkFile; +import org.alfresco.jlan.server.filesys.quota.QuotaManager; +import org.alfresco.jlan.server.filesys.quota.QuotaManagerException; import org.alfresco.jlan.server.locking.FileLockingInterface; import org.alfresco.jlan.server.locking.LockManager; import org.alfresco.jlan.server.locking.OpLockInterface; @@ -650,6 +653,22 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa } else logger.warn("Oplock support disabled for filesystem " + ctx.getDeviceName()); + + // Start the quota manager, if enabled + + if ( context.hasQuotaManager()) { + + try { + + // Start the quota manager + + context.getQuotaManager().startManager( this, context); + logger.info("Quota manager enabled for filesystem"); + } + catch ( QuotaManagerException ex) { + logger.error("Failed to start quota manager", ex); + } + } } /** @@ -2261,6 +2280,25 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa } } + // Check if there is a quota manager enabled + + long fileSize = 0L; + + if ( ctx.hasQuotaManager() && file.hasDeleteOnClose()) { + + // Make sure the content stream has been opened, to get the current file size + + if ( file instanceof ContentNetworkFile) { + ContentNetworkFile contentFile = (ContentNetworkFile) file; + if ( contentFile.hasContent() == false) + contentFile.openContent( false, false); + + // Save the current file size + + fileSize = contentFile.getFileSize(); + } + } + // Defer to the network file to close the stream and remove the content file.closeFile(); @@ -2289,6 +2327,11 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Delete the file fileFolderService.delete(nodeRef); + + // Check if there is a quota manager enabled, release space back to the user quota + + if ( ctx.hasQuotaManager()) + ctx.getQuotaManager().releaseSpace(sess, tree, file.getFileId(), file.getFullName(), fileSize); } catch ( Exception ex) { @@ -2376,6 +2419,18 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa try { + // Check if there is a quota manager enabled, if so then we need to save the current file size + + QuotaManager quotaMgr = ctx.getQuotaManager(); + FileInfo fInfo = null; + + if ( quotaMgr != null) { + + // Get the size of the file being deleted + + fInfo = getFileInformation( sess, tree, name); + } + // Get the node NodeRef nodeRef = getNodeForPath(tree, name); @@ -2427,6 +2482,11 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa parentState.updateModifyDateTime(); } } + + // Release the space back to the users quota + + if ( quotaMgr != null) + quotaMgr.releaseSpace( sess, tree, fInfo.getFileId(), name, fInfo.getSize()); } // Debug @@ -2951,14 +3011,75 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa */ public void truncateFile(SrvSession sess, TreeConnection tree, NetworkFile file, long size) throws IOException { - // Truncate or extend the file to the required size + // Keep track of the allocation/release size in case the file resize fails - file.truncateFile(size); - - // Debug - ContentContext ctx = (ContentContext) tree.getContext(); + long allocSize = 0L; + long releaseSize = 0L; + + // Check if there is a quota manager + + QuotaManager quotaMgr = ctx.getQuotaManager(); + + if ( ctx.hasQuotaManager()) { + + // Check if the file content has been opened, we need the content to be opened to get the + // current file size + + if ( file instanceof ContentNetworkFile) { + ContentNetworkFile contentFile = (ContentNetworkFile) file; + if ( contentFile.hasContent() == false) + contentFile.openContent( false, false); + } + else + throw new IOException("Invalid file class type, " + file.getClass().getName()); + + // Determine if the new file size will release space or require space allocating + + if ( size > file.getFileSize()) { + + // Calculate the space to be allocated + + allocSize = size - file.getFileSize(); + + // Allocate space to extend the file + + quotaMgr.allocateSpace(sess, tree, file, allocSize); + } + else { + + // Calculate the space to be released as the file is to be truncated, release the space if + // the file truncation is successful + + releaseSize = file.getFileSize() - size; + } + } + + // Set the file length + + try { + file.truncateFile(size); + } + catch (IOException ex) { + + // Check if we allocated space to the file + + if ( allocSize > 0 && quotaMgr != null) + quotaMgr.releaseSpace(sess, tree, file.getFileId(), null, allocSize); + + // Rethrow the exception + + throw ex; + } + + // Check if space has been released by the file resizing + + if ( releaseSize > 0 && quotaMgr != null) + quotaMgr.releaseSpace(sess, tree, file.getFileId(), null, releaseSize); + + // Debug + if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILEIO)) logger.debug("Truncated file: network file=" + file + " size=" + size); } @@ -3071,15 +3192,52 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa if ( contentFile.hasContent() == false) beginReadTransaction( sess); } + + // Check if there is a quota manager + + ContentContext ctx = (ContentContext) tree.getContext(); + QuotaManager quotaMgr = ctx.getQuotaManager(); + long curSize = file.getFileSize(); + + if ( quotaMgr != null) { + + // Check if the file requires extending + + long extendSize = 0L; + long endOfWrite = fileOffset + size; + + if ( endOfWrite > curSize) { + + // Calculate the amount the file must be extended + + extendSize = endOfWrite - file.getFileSize(); + + // Allocate space for the file extend + + quotaMgr.allocateSpace(sess, tree, file, extendSize); + } + } // Write to the file file.writeFile(buffer, size, bufferOffset, fileOffset); + + // Check if the file size was reduced by the write, may have been extended previously + + if ( quotaMgr != null) { + + // Check if the file size reduced + + if ( file.getFileSize() < curSize) { + + // Release space that was freed by the write + + quotaMgr.releaseSpace( sess, tree, file.getFileId(), file.getFullName(), curSize - file.getFileSize()); + } + } // Debug - ContentContext ctx = (ContentContext) tree.getContext(); - if (logger.isDebugEnabled() && ctx.hasDebug(AlfrescoContext.DBG_FILEIO)) logger.debug("Wrote bytes to file: network file=" + file + " buffer size=" + buffer.length + " size=" + size + " file offset=" + fileOffset); diff --git a/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java b/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java index d48c88f083..8af6d735d5 100644 --- a/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java +++ b/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java @@ -273,7 +273,7 @@ public class ContentNetworkFile extends NodeRefNetworkFile * @see NetworkFile#WRITEONLY * @see NetworkFile#READWRITE */ - private void openContent(boolean write, boolean trunc) + protected void openContent(boolean write, boolean trunc) throws AccessDeniedException, AlfrescoRuntimeException { // Check if the file is a directory @@ -359,6 +359,17 @@ public class ContentNetworkFile extends NodeRefNetworkFile channel = ((ContentReader) content).getFileChannel(); } + + // Update the current file size + + if ( channel != null) { + try { + setFileSize(channel.size()); + } + catch (IOException ex) { + logger.error( ex); + } + } } /** diff --git a/source/java/org/alfresco/filesys/repo/ContentQuotaManager.java b/source/java/org/alfresco/filesys/repo/ContentQuotaManager.java new file mode 100644 index 0000000000..8dbfedbdd8 --- /dev/null +++ b/source/java/org/alfresco/filesys/repo/ContentQuotaManager.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.filesys.repo; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; + +import org.alfresco.filesys.state.FileStateTable; +import org.alfresco.jlan.server.SrvSession; +import org.alfresco.jlan.server.filesys.DiskDeviceContext; +import org.alfresco.jlan.server.filesys.DiskFullException; +import org.alfresco.jlan.server.filesys.DiskInterface; +import org.alfresco.jlan.server.filesys.NetworkFile; +import org.alfresco.jlan.server.filesys.TreeConnection; +import org.alfresco.jlan.server.filesys.quota.QuotaManager; +import org.alfresco.jlan.server.filesys.quota.QuotaManagerException; +import org.alfresco.jlan.util.MemorySize; +import org.alfresco.service.cmr.usage.ContentUsageService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Content Quota Manager Class + * + *

Quota manager implementation for the Alfresco repository. + * + * @author gkspencer + * + */ +public class ContentQuotaManager implements QuotaManager, Runnable { + + // Debug logging + + private static final Log logger = LogFactory.getLog(ContentQuotaManager.class); + + // User details idle check interval + + private static final long UserQuotaCheckInterval = 1 * 60 * 1000; // 1 minute + private static final long UserQuotaExpireInterval = 5 * 60 * 1000; // 5 minutes + + // Associated filesystem driver + + private ContentDiskDriver m_filesys; + + // Content usage service + + private ContentUsageService m_usageService; + + // Track live usage of users that are writing files + + private HashMap m_liveUsage; + private Object m_addDetailsLock = new Object(); + + // User details inactivity checker thread + + private Thread m_thread; + private boolean m_shutdown; + + /** + * Get the usage service + * + * @return ContentUsageService + */ + public final ContentUsageService getUsageService() { + return m_usageService; + } + + /** + * Set the usage service + * + * @param usageService ContentUsageService + */ + public final void setUsageService(ContentUsageService usageService) { + m_usageService = usageService; + } + + /** + * Return the free space available in bytes + * + * @return long + */ + public long getAvailableFreeSpace() { + + // Return a dummy value for now + // + // Need to find the content store size and return the live available space value if possible + + return 100 * MemorySize.GIGABYTE; + } + + /** + * Return the free space available to the specified user/session + * + * @param sess SrvSession + * @param tree TreeConnection + * @return long + */ + public long getUserFreeSpace(SrvSession sess, TreeConnection tree) { + + // Check if there is a live usage record for the user + + UserQuotaDetails userQuota = getQuotaDetails(sess, true); + if ( userQuota != null) + return userQuota.getAvailableSpace(); + + // No quota details available + + return 0L; + } + + /** + * Allocate space on the filesystem. + * + * @param sess SrvSession + * @param tree TreeConnection + * @param file NetworkFile + * @param alloc long + * @return long + * @exception IOException + */ + public long allocateSpace(SrvSession sess, TreeConnection tree, NetworkFile file, long alloc) + throws IOException { + + // Check if there is a live usage record for the user + + UserQuotaDetails userQuota = getQuotaDetails(sess, true); + long allowedAlloc = 0L; + + if ( userQuota != null) { + + // Check if the user has a usage quota + + if ( userQuota.hasUserQuota()) { + + synchronized ( userQuota) { + + // Check if the user has enough free space allocation + + if ( alloc > 0 && userQuota.getAvailableSpace() >= alloc) { + userQuota.addToCurrentUsage( alloc); + allowedAlloc = alloc; + } + } + } + else { + + // Update the live usage + + synchronized ( userQuota) { + userQuota.addToCurrentUsage( alloc); + allowedAlloc = alloc; + } + } + } + else if ( logger.isDebugEnabled()) + logger.debug("Failed to allocate " + alloc + " bytes for sess " + sess.getUniqueId()); + + // Check if the allocation was allowed + + if ( allowedAlloc < alloc) { + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Allocation failed userQuota=" + userQuota); + + throw new DiskFullException(); + } + else if ( logger.isDebugEnabled()) + logger.debug("Allocated " + alloc + " bytes, userQuota=" + userQuota); + + // Return the allocation size + + return allowedAlloc; + } + + /** + * Release space to the free space for the filesystem. + * + * @param sess SrvSession + * @param tree TreeConnection + * @param fid int + * @param path String + * @param alloc long + * @exception IOException + */ + public void releaseSpace(SrvSession sess, TreeConnection tree, int fid, String path, long alloc) + throws IOException { + + // Check if there is a live usage record for the user + + UserQuotaDetails userQuota = getQuotaDetails(sess, true); + + if ( userQuota != null) { + + synchronized ( userQuota) { + + // Release the space from the live usage value + + userQuota.subtractFromCurrentUsage( alloc); + } + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Released " + alloc + " bytes, userQuota=" + userQuota); + } + else if ( logger.isDebugEnabled()) + logger.debug("Failed to release " + alloc + " bytes for sess " + sess.getUniqueId()); + } + + /** + * Start the quota manager. + * + * @param disk DiskInterface + * @param ctx DiskDeviceContext + * @exception QuotaManagerException + */ + public void startManager(DiskInterface disk, DiskDeviceContext ctx) + throws QuotaManagerException { + + // Save the filesystem driver details + + if ( disk instanceof ContentDiskDriver) + m_filesys = (ContentDiskDriver) disk; + else + throw new QuotaManagerException("Invalid filesystem type, " + disk.getClass().getName()); + + // Allocate the live usage table + + m_liveUsage = new HashMap(); + + // Create the inactivity checker thread + + m_thread = new Thread(this); + m_thread.setDaemon(true); + m_thread.setName("ContentQuotaManagerChecker"); + m_thread.start(); + } + + /** + * Stop the quota manager + * + * @param disk DiskInterface + * @param ctx DiskDeviceContext + * @exception QuotaManagerException + */ + public void stopManager(DiskInterface disk, DiskDeviceContext ctx) + throws QuotaManagerException { + + // Clear out the live usage details + + m_liveUsage.clear(); + + // Shutdown the checker thread + + m_shutdown = true; + m_thread.interrupt(); + } + + /** + * Get the usage details for the session/user + * + * @param sess SrvSession + * @param loadDetails boolean + * @return UserQuotaDetails + */ + private UserQuotaDetails getQuotaDetails(SrvSession sess, boolean loadDetails) { + + UserQuotaDetails userQuota = null; + if ( sess != null && sess.hasClientInformation()) { + + // Get the live usage values + + userQuota = m_liveUsage.get( sess.getClientInformation().getUserName()); + if ( userQuota == null && loadDetails == true) { + + // User is not in the live tracking table, load details for the user + + try { + userQuota = loadUsageDetails( sess); + } + catch ( QuotaManagerException ex) { + if ( logger.isDebugEnabled()) + logger.debug( ex); + } + } + } + + // Return the user quota details + + return userQuota; + } + + /** + * Load the user quota details + * + * @param sess SrvSession + * @return UserQuotaDetails + * @throws QuotaManagerException + */ + private UserQuotaDetails loadUsageDetails(SrvSession sess) + throws QuotaManagerException { + + // Check if the user name is available + + if ( sess == null || sess.hasClientInformation() == false) + throw new QuotaManagerException("No session/client information"); + + UserQuotaDetails quotaDetails = null; + String userName = null; + + try { + + // Get the user name + + userName = sess.getClientInformation().getUserName(); + if ( userName == null || userName.length() == 0) + throw new QuotaManagerException("No user name for client"); + + // Start a transaction + + m_filesys.beginReadTransaction(sess); + + // Get the usage quota and current usage values for the user + + long userQuota = m_usageService.getUserQuota( userName); + long userUsage = m_usageService.getUserUsage( userName); + + // Create the user quota details for live tracking + + quotaDetails = new UserQuotaDetails( userName, userQuota); + if ( userUsage > 0L) + quotaDetails.setCurrentUsage( userUsage); + + // Add the details to the live tracking table + + synchronized ( m_addDetailsLock) { + + // Check if another thread has added the details + + UserQuotaDetails details = m_liveUsage.get( userName); + if ( details != null) + quotaDetails = details; + else + m_liveUsage.put( userName, quotaDetails); + } + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug( "Added live usage tracking " + quotaDetails); + } + catch ( Exception ex) { + + // Log the error + + if ( logger.isErrorEnabled()) + logger.error( ex); + + // Failed to load usage details + + throw new QuotaManagerException("Failed to load usage for " + userName + ", " + ex); + } + + // Return the user usage details + + return quotaDetails; + } + + /** + * Inactivity checker, run in a seperate thread + */ + public void run() { + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Content quota manager checker thread starting"); + + // Loop forever + + m_shutdown = false; + + while ( m_shutdown == false) + { + + // Sleep for the required interval + + try + { + Thread.sleep( UserQuotaCheckInterval); + } + catch (InterruptedException ex) + { + } + + // Check for shutdown + + if ( m_shutdown == true) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug("Content quota manager checker thread closing"); + + return; + } + + // Check if there are any user quota details to check + + if ( m_liveUsage != null && m_liveUsage.size() > 0) + { + try + { + // Timestamp to check if the quota details is inactive + + long checkTime = System.currentTimeMillis() - UserQuotaExpireInterval; + + // Loop through the user quota details + + Iterator userNames = m_liveUsage.keySet().iterator(); + + while ( userNames.hasNext()) { + + // Get the user quota details and check if it has been inactive in the last check interval + + String userName = userNames.next(); + UserQuotaDetails quotaDetails = m_liveUsage.get( userName); + + if ( quotaDetails.getLastUpdated() < checkTime) { + + // Remove the live usage tracking details, inactive + + m_liveUsage.remove( userName); + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Removed inactive usage tracking, " + quotaDetails); + } + } + } + catch (Exception ex) + { + // Log errors if not shutting down + + if ( m_shutdown == false) + logger.debug(ex); + } + } + } + } +} diff --git a/source/java/org/alfresco/filesys/repo/UserQuotaDetails.java b/source/java/org/alfresco/filesys/repo/UserQuotaDetails.java new file mode 100644 index 0000000000..eeeafeec3d --- /dev/null +++ b/source/java/org/alfresco/filesys/repo/UserQuotaDetails.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.filesys.repo; + +import org.alfresco.jlan.util.MemorySize; + +/** + * User Quota Details Class + * + *

Used to track the live usage of a user as files are being written. + * + * @author gkspencer + */ +public class UserQuotaDetails { + + // User name and allowed quota, -1 indicates unlimited quota + + private String m_userName; + private long m_quota; + + // Current live usage + + private long m_curUsage; + + // Timestamp of the last allocation/release + + private long m_lastUpdate; + + /** + * Class constructor + * + * @param userName String + * @param quota long + */ + public UserQuotaDetails(String userName, long quota) { + m_userName = userName; + m_quota = quota; + } + + /** + * Return the user name + * + * @return String + */ + public final String getUserName() { + return m_userName; + } + + /** + * Check if the user has a usage quota + * + * @return boolean + */ + public final boolean hasUserQuota() { + return m_quota == -1L ? false : true; + } + + /** + * Return the user quota, in bytes + * + * @return long + */ + public final long getUserQuota() { + return m_quota; + } + + /** + * Return the current live usage, in bytes + * + * @return long + */ + public final long getCurrentUsage() { + return m_curUsage; + } + + /** + * Return the time the live usage value was last updated + * + * @return long + */ + public final long getLastUpdated() { + return m_lastUpdate; + } + + /** + * Return the available space for this user, -1 is unlimited + * + * @return long + */ + public final long getAvailableSpace() { + if ( getUserQuota() == 0) + return -1L; + long availSpace = getUserQuota() - getCurrentUsage(); + if ( availSpace < 0L) + availSpace = 0L; + return availSpace; + } + + /** + * Set the user quota, in bytes + * + * @param quota long + */ + public final void setUserQuota(long quota) { + m_quota = quota; + } + + /** + * Update the current live usage + * + * @param usage long + */ + public final void setCurrentUsage(long usage) { + m_curUsage = usage; + m_lastUpdate = System.currentTimeMillis(); + } + + /** + * Add to the current live usage + * + * @param usage long + * @return long + */ + public final long addToCurrentUsage(long usage) { + m_curUsage += usage; + m_lastUpdate = System.currentTimeMillis(); + + return m_curUsage; + } + + /** + * Subtract from the current live usage + * + * @param usage long + * @return long + */ + public final long subtractFromCurrentUsage(long usage) { + m_curUsage -= usage; + m_lastUpdate = System.currentTimeMillis(); + + return m_curUsage; + } + + /** + * Return the user quota details as a string + * + * @return String + */ + public String toString() { + StringBuilder str = new StringBuilder(); + + str.append("["); + str.append(getUserName()); + str.append(",quota="); + str.append(MemorySize.asScaledString(getUserQuota())); + str.append(",current="); + str.append(getCurrentUsage()); + str.append(",available="); + str.append(getAvailableSpace()); + str.append("/"); + str.append(MemorySize.asScaledString(getAvailableSpace())); + str.append("]"); + + return str.toString(); + } +}