FileSystemDaemon.java
/*
** Module : FileSystemDaemon.java
** Abstract : worker object that handles delegated file system processing
**
** Copyright (c) 2006-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 GES 20060211 @24539 Created initial version, which completely
** implements the FileSystem interface.
** 002 GES 20070914 @35141 Refactored the propath searching code to
** use a static worker. This allows the same
** routine to be used at conversion time for
** searching.
** 003 GES 20080118 @36862 Added NPE protection logic in search().
** 004 GES 20080123 @36903 More NPE protection logic.
** 005 GES 20080321 @37630 Added more data (almost complete) to the
** FILE-INFO support.
** 006 GES 20110712 Searching now honors the case-sensitivity of the
** legacy system.
** 007 GES 20111025 Modified API export code to capture and use the
** authentication token so that multiple registrations
** can be done per JVM session. This occurs when the
** client code is running in the server process and
** there is a CTRL-C that causes the client to
** restart.
** 008 GES 20111111 Modified interface export to allow registration
** to be exclusively local or remote as needed.
** 009 CS 20121104 Implement PROPATH assignment.
** 010 CS 20130115 Task 1613: finish FILE-INFO system handle support
** 011 CA 20140313 Fixed the regexp for win-separator replacement.
** Added delete(File, recursive).
** 012 ECF 20150715 Replace StringBuffer with StringBuilder.
** 013 EVL 20151026 Adding more criteria to detect file path relative to the current
** directory. For UNIX-like systems added the path beginning not with
** File separator. For Windows systems the current path must not start
** with File separator and root drive letter + ':\\'. The canonical
** name is now used for this case too in getCaseInsensitiveMatch()
** filename transformation. If root or current path search is failed
** the PROPATH entries are used as last resort.
** 014 IAS 20160113 Added image files' search with different extensions
** 015 EVL 20160329 Adding the ability to check the environment variable from OS first
** and if not found - use defined in JVM or via -D java start option.
** 016 OM 20160428 Added searchAll() method.
** 017 SVL 20171109 Added getFileContents.
** 018 HC 20171228 Fixed normalization of file path separators.
** 019 ECF 20180109 Normalize file separators for file copy.
** GES 20180115 Added normalization to all other filename inputs so that all forms
** of usage are safe.
** 020 EVL 20180205 Fix for Windows Matcher.replaceAll() method issue with file
** separator as input parameter.
** 021 SBI 20180518 Added PathResolver usages in order to resolve a legacy file system
** path on the host system.
** CA 20180520 Fixed getFileName - if the file/folder exists, use it, regardless of
** case-sensitivity.
** 022 EVL 20180530 Adding support for (GET|SET)-WORKING-DIRECTORY function.
** ECF 20180609 Removed static modifier from {get|set}WorkingDirectory methods.
** Reorganized methods to meet coding standards.
** 023 SBI 20180921 Changed mkdir to do a case-insensitive search of existing parents.
** 024 AIL 20200211 Empty path for mkdir should return no-error.
** 025 AIL 20210813 Added getSearchPath() method.
** CA 20220220 Fixed copy - if the target is a directory, append the source filename to it.
** 026 HC 20230427 Implemented server-side file-system resource. File operations
** performed on client can be optionally performed directly on server
** without the expensive remote call.
** 027 CA 20230510 'fsd' may be null if 'getFileName' is called from conversion.
** 028 RFB 20231018 Added capability to handle a path starting with ~/ in normalizeFileSeparators.
** Ref. #7858.
** 029 GBB 20240826 Renaming 'single' to 'local' (single client removed).
** Moving getTempDirectory() here from OsPropertiesDaemon.
** 030 GBB 20240912 Moving file / dir stream methods to FileSystem.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.util.osresource;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.regex.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;
/**
* Implements file-system utility methods (for query and manipulation) in a
* manner that is suitable for use on a remote node. Construction of this
* object causes these APIs to be exported from the current node.
*/
public class FileSystemDaemon
implements FileSystem
{
/** Regexp pattern for matching any path separators */
private static final Pattern anyPathSep = Pattern.compile(ANY_PATH_SEPARATOR);
/**
* Maximum size of file that can be transferred using {@link #getFileContents(String)}, in
* bytes.
*/
private static final int MAXIMUM_FILE_TRANSFER_SIZE = 5 * 1024 * 1024;
/** Flag to note if the rutnime platform for this JVM is Windows. */
private static final boolean isWin = PlatformHelper.isUnderWindowsFamily();
/** The singleton instance of this class */
private static final ContextLocal<FileSystemDaemon> instance = new ContextLocal<>();
/** Token used to authenticate with the dispatcher when registering APIs. */
private Object modToken = null;
/** Cached server-side flag */
private final boolean isServerSideFS;
/** Paths to search for relative filenames. */
private String[] paths = null;
/** The legacy system's filesystem case-sensitivity. */
private boolean caseSens = true;
/** The legacy path resolver */
private PathResolverContainer pathResolver;
/** File system resource plugin */
private FileSystemResource fsRes;
/**
* Create an instance and export its API to the network.
*
* @param local
* <code>true</code> to register purely on a local basis (no remote/network access to these
* methods).
* @param serverSideFS
* <code>true</code> if the server-side file system is being used.
*/
private FileSystemDaemon(boolean local, boolean serverSideFS)
{
synchronized (FileSystemDaemon.class)
{
modToken = RemoteObject.registerServer(FileSystem.class, this, modToken, local);
SecurityManager sm = SecurityManager.getInstance();
fsRes = serverSideFS ?
(FileSystemResource) sm.getMandatoryPluginInstance(FileSystemResource.TYPE_NAME) :
(FileSystemResource) sm.getPluginInstance(FileSystemResource.TYPE_NAME);
isServerSideFS = serverSideFS;
}
}
/**
* Retrieve the target instance.
*
* @param local
* <code>true</code> to register purely on a local basis (no remote/network access to these
* methods).
* @param isServerSideFS
* <code>true</code> if the server-side file system is being used.
*/
static synchronized FileSystemDaemon getInstance(boolean local, boolean isServerSideFS)
{
FileSystemDaemon fsd = instance.get();
if (fsd == null)
{
fsd = new FileSystemDaemon(local, isServerSideFS);
instance.set(fsd);
}
return fsd;
}
/**
* Search for the given file in each directory listed in the paths specified. If the file
* system object exists and is a file, then the filename is returned. If the path list contains
* an empty string or a '.', then the current directory is searched, otherwise only the listed
* directories are searched.
* <p>
* If the filename is specified as an absolute name, this specific filename is checked and no
* path searching is done.
*
* @param paths
* The list of paths to search. The file separator character used in these paths
* should be the same as the current system's file separator character.
* @param caseSens
* {@code true} if the search should honor a case-sensitive file-system. When set to
* {@code false} and the current filesystem is naturally case-sensitive, a
* case-insensitive search will be implemented.
* @param filename
* The absolute or relative filename for which to search.
* @param allowDir
* If {@code true} the search will also directories, if {@code false} only file
* results will be returned.
*
* @return If the file is found in the current directory, the relative filename is returned.
* If the file is absolute or found in any other path, the full absolute filename is
* returned. If the file is not found or the referenced file system object is not a
* file, {@code null} is returned.
*/
public static String search(String[] paths,
boolean caseSens,
String filename,
boolean allowDir)
{
return search(paths, caseSens, filename, allowDir, false);
}
/**
* Search for the target path name. If the target path name is an absolute name or starts from
* the dot path name, then this specific filename is checked and no path searching is done if
* the special searchAbsoluteOrRelativeOnly is set on. Otherwise the given path list is used
* to resolve the target path name against them and the first matched file is returned.
*
* @param paths
* The list of paths to search. The file separator character used in these paths
* should be the same as the current system's file separator character.
* @param caseSens
* {@code true} if the search should honor a case-sensitive file-system. When set to
* {@code false} and the current filesystem is naturally case-sensitive, a
* case-insensitive search will be implemented.
* @param filename
* The absolute or relative filename for which to search.
* @param allowDir
* If {@code true} the search will also directories, if {@code false} only file
* results will be returned.
* @param searchAbsoluteOrRelativeOnly
* The true value indicates that the given absolute or relative path name is only
* looked up, otherwise the given list of paths are used to look up the target path.
*
* @return If the file is found in the current directory, the relative filename is returned.
* If the file is absolute or found in any other path, the full absolute filename is
* returned. If the file is not found or the referenced file system object is not a
* file, {@code null} is returned.
*/
public static String search(String[] paths,
boolean caseSens,
String filename,
boolean allowDir,
boolean searchAbsoluteOrRelativeOnly)
{
// make sure there is something to search for (the code below needs to be protected)
if (filename == null)
{
return null;
}
// if the the OS is windows replace all / to \ from the search paths
String fn = normalizeFileSeparators(filename);
String result = getAbsoluteOrCurrent(caseSens, fn, allowDir);
if (result != null || searchAbsoluteOrRelativeOnly)
{
return result;
}
// not found in root or current, try PROPATH entries
if (paths == null)
{
return null;
}
return Utils.tryWhileNull(Arrays.asList(paths),
path -> getInPath(caseSens, path, fn, allowDir));
}
/**
* Return all files matching the specified {@code filename} from each directory listed in the
* {@code paths} specified. If the path list contains an empty string or a '.', then the
* current directory is searched, otherwise only the listed directories are searched.
* <p>
* If the filename is specified as an absolute name, this specific filename is checked and no
* path searching is done and returned as a single element array.
*
* @param paths
* The list of paths to search. The file separator character used in these paths
* should be the same as the current system's file separator character.
* @param caseSens
* {@code true} if the search should honor a case-sensitive file-system. When set to
* {@code false} and the current filesystem is naturally case-sensitive, a
* case-insensitive search will be implemented.
* @param filename
* The absolute or relative filename for which to search.
* @param allowDir
* If {@code true} the search will also directories, if {@code false} only file(s)
* results will be returned.
*
* @return The relative filename for all matching files are returned as {@code String} list.
* If the file is absolute or found in any other path, the full absolute filename is
* returned as a single element. If the file is not found or the referenced file
* system object is not a file, {@code null} is returned.
*/
public static List<String> searchAll(String[] paths,
boolean caseSens,
String filename,
boolean allowDir)
{
// make sure there is something to search for (the code below needs to be protected)
if (filename == null)
{
return null;
}
// if the the OS is windows replace all / to \ from the search paths
String fn = normalizeFileSeparators(filename);
String result = getAbsoluteOrCurrent(caseSens, fn, allowDir);
if (result != null)
{
List<String> ret = new ArrayList<>(1);
ret.add(result);
return ret;
}
// not found in root or current, try PROPATH entries
if (paths == null)
{
return null;
}
ArrayList<String> ret = new ArrayList<>(paths.length);
for (String path : paths)
{
String fileName = getInPath(caseSens, path, fn, allowDir);
if (fileName != null)
{
ret.add(fileName);
}
}
ret.trimToSize();
return ret.isEmpty() ? null : ret;
}
/**
* Gets the OS environment variable value for given name if devined.
*
* @return The currently defines ins OS environment value or <code>null</code> if variable
* is currentlu not defined for given process.
*/
public static native String getEnvironmentValue(String vname);
/**
* Gets the working directory from OS.
*
* @return The current working directory.
*/
public static native String getWorkingDir();
/**
* Sets the working directory via native OS call.
*
* @param newWorkingDir
* A new working directory to set.
*
* @return <code>TRUE</code> if success, <code>FALSE</code> otherwise.
*/
public static native boolean setWorkingDir(String newWorkingDir);
/**
* Replaces UNIX-style and Windows-style file separators with {@link File#separator}.
* Also expands a file name that begins with "~/" to the user.home system property on UNIX platform.
*
* @param fileName
* A file name.
*
* @return Returns the normalized file name.
*/
public static String normalizeFileSeparators(String fileName)
{
// Check if the fileName starts with a tilde and path separator ('~/')
if (!isWin && fileName.startsWith("~") && (fileName.length() == 1 || fileName.charAt(1) == '/'))
{
// Get the "user.home" property
String userHome = System.getProperty("user.home");
// Create the expanded components
fileName = userHome + fileName.substring(1);
}
Matcher m = anyPathSep.matcher(fileName);
return m.replaceAll(isWin ? "\\".concat(File.separator) : File.separator);
}
/**
* Find the actual canonical filesystem name that matches the given file or
* directory, if a case-insensitive match exists.
*
* @param file
* The filesystem object to search for.
* @param isDir
* <code>true</code> if searching for a directory and
* <code>false</code> if searching for a file.
*
* @return The filesystem name found or <code>null</code> if no such
* object exists.
*/
public static String getCaseInsensitiveMatch(File file, boolean isDir)
{
String result = null;
try
{
// if the file is absolute use canonical path, otherwise use regular
String fileName = file.getCanonicalPath();
// case-insensitive search
List<File> list = Utils.getCaseInsensitiveFilenameMatches(fileName);
// use the first match that is valid
for (File next : list)
{
if (next.exists())
{
if ((!isDir && next.isFile()) || (isDir && next.isDirectory()))
{
result = next.getCanonicalPath();
break;
}
}
}
}
catch (IOException exc)
{
// TODO: add reporting/logging
// file system error, ignore
}
return result;
}
/**
* Deletes the specified file system entity, if necessary using recursion to delete all
* contents (if it is a directory) first. If any error occurs, no exception will be thrown.
*
* @param target
* The target file or directory to delete.
* @param recursive
* Recursively decend through all subdirectories, deleting the entire subtree.
*
* @return The error code (<code>ERR_NO_ERROR</code> if no problems occur).
*/
public static int delete(File target, boolean recursive)
{
return deleteWorker(target, recursive);
}
/**
* Deletes the specified file system entity, if necessary using recursion
* to delete all contents (if it is a directory) first. If any error
* occurs, no exception will be thrown.
*
* @param target
* The target file or directory to delete.
* @param recursive
* Recursively decend through all subdirectories, deleting
* the entire subtree.
*
* @return The error code (<code>ERR_NO_ERROR</code> if no problems
* occur).
*/
private static int deleteWorker(File target, boolean recursive)
{
try
{
String targetPath;
try
{
targetPath = target.getCanonicalPath();
}
catch (IOException e)
{
return ERR_IO;
}
// check the permissions yet before existence check, don't give any clues about the file system
FileSystemDaemon inst = instance.get();
if (inst != null && inst.isServerSideFS && !inst.fsRes.canDelete(targetPath))
{
return ERR_PERM_DENIED;
}
if (!target.exists())
{
// we can't delete what isn't there
return ERR_NO_SUCH_FILE_OR_DIR;
}
if (target.isDirectory())
{
// delete contents recursively if requested
if (recursive)
{
File[] children = target.listFiles();
if (children == null)
{
return ERR_IO;
}
for (int i = 0; i < children.length; i++)
{
deleteWorker(children[i], recursive);
}
}
// we can't delete a non-empty directory (if recursive was true,
// then some child failed to delete, if false then the caller
// shouldn't have expected this to work)
if (target.list().length > 0)
{
return ERR_DIR_NOT_EMPTY;
}
}
// we are a leaf or we are an empty directory, try to delete target
// (this must occur after all children are processed in the case of
// a directory)
boolean result = target.delete();
if (!result)
{
// the deletion failed
return ERR_UNKNOWN;
}
}
catch (SecurityException se)
{
return ERR_PERM_DENIED;
}
return ERR_NO_ERROR;
}
/**
* Utility method that checks if a path is in a form that is relative to the current
* directory.
*
* @param path
* The path to check. No call to {@link #normalizeFileSeparators} will be made. It
* is assumed this is already normalized.
*
* @return <code>true</code> if the path is in a form that is relative to current directory
* or <code>false</code> otherwise.
*/
private static boolean isPathRelativeToCurrentDir(String path)
{
String sep = File.separator;
// TODO: In the code below (!path.startsWith(":" + sep, 1)) shouldn't we allow for a
// case where there is just the drive letter (C:)? When that drive is the same as
// the drive for the current directory, then it is a form of relative reference.
// But when the current directory is on a different drive, then this result seems
// wrong.
// check if the path is relative to the current dir
return (path.startsWith("." + sep) ||
path.startsWith(".." + sep) ||
path.equals(".") ||
path.equals("..") ||
(!isWin && !path.startsWith(sep)) ||
(isWin && (!path.startsWith(sep) && !path.startsWith(":" + sep, 1))));
}
/**
* Return file name if it is absolute or exists in the current directory.
*
* @param caseSens
* <code>true</code> if file system is case-sensitive
* @param filename
* The file to check. No call to {@link #normalizeFileSeparators} will be made. It
* is assumed this is already normalized.
* @param allowDir
* <code>true</code> if directories are accepted
*
* @return The file name if it is absolute or exists in the current directory.
*/
private static String getAbsoluteOrCurrent(boolean caseSens,
String filename,
boolean allowDir)
{
FileSystemDaemon fsd = instance.get();
if (fsd != null)
{
filename = fsd.resolvePath(filename);
}
File file = new File(filename);
if (file.isAbsolute() || isPathRelativeToCurrentDir(filename))
{
return getFileName(caseSens, allowDir, file, filename);
}
return null;
}
/**
* Return the file name if the file is valid in the given path.
*
* @param caseSens
* <code>true</code> if file system is case-sensitive
* @param path
* The path to be checked. This will be normalized before processing by calling
* {@link #normalizeFileSeparators}.
* @param allow
* <code>true</code> if directories are accepted.
* @param filename
* The file to check. No call to {@link #normalizeFileSeparators} will be made. It
* is assumed this is already normalized.
*
* @return The file name if file is valid in the given path.
*/
private static String getInPath(boolean caseSens, String path, String filename, boolean allow)
{
if (path == null)
{
return null;
}
FileSystemDaemon fsd = instance.get();
if (fsd != null)
{
filename = fsd.resolvePath(filename);
path = fsd.resolvePath(path);
}
File file = new File(filename);
// check if path is empty or if it is a file
if (path.length() != 0 || !file.isFile())
{
path = normalizeFileSeparators(path);
// if we have to replace separator also do this for PROPATH paths
file = new File(path, filename);
}
return getFileName(caseSens, allow, file, file.getPath());
}
/**
* Return the given file name if file/directory exists and matches the given criteria.
*
* @param caseSens
* <code>true</code> if file system is case-sensitive.
* @param allowDir
* <code>true</code> if directories are accepted.
* @param file
* The file to be checked.
* @param filename
* The name to return if it exists and matches the rest of the criteria.
* No call to {@link #normalizeFileSeparators} will be made. It is
* assumed this is already normalized.
*
* @return file name if file is valid
*/
private static String getFileName(boolean caseSens,
boolean allowDir,
File file,
String filename)
{
FileSystemDaemon fsd = instance.get();
try
{
if (fsd != null && fsd.isServerSideFS)
{
File absFile = file.getAbsoluteFile();
if (!fsd.fsRes.canRead(absFile.getCanonicalPath()))
{
return null;
}
}
}
catch (IOException e)
{
return null;
}
return (file.exists() && (file.isFile() || allowDir)) ? filename : caseSens ? null :
getCaseInsensitiveMatch(file, false);
}
/**
* Constructs a remote stream instance representing a file using a filename, this file or
* device will be opened for reading or writing based on the given <code>write</code> flag.
* The current read/write position will be at offset 0 unless in <code>write</code> mode and
* the <code>append</code> flag is on (in which case the write position will be at the end of
* the file).
* <p>
* This stream will NOT have paging active.
*
* @param filename
* The name of the file or device to open. Must not be <code>null</code>.
* @param write
* If <code>true</code>, open in write mode, otherwise open the file for reading.
* @param append
* In <code>write</code> mode, if <code>true</code>, the current file pointer is
* set to the end of the file. Otherwise if in <code>write</code> mode, the file
* is truncated to 0 length. This value is ignored in <code>read</code> mode.
* @param hasDuplicatedLastLine
* Flag to indicate if the last line in the file is duplicate (true for LOB files).
*
* @return The stream id which is used to route operations to the newly created instance.
*
* @throws ErrorConditionException
* If the file does not exist OR if the current user does not have the proper rights
* to open the file or device.
*/
@Override
public int openFileStream(String filename, boolean write, boolean append, boolean hasDuplicatedLastLine)
throws ErrorConditionException
{
boolean isTerminal = false;
if (filename != null)
{
filename = resolvePathName(filename, false);
String term = filename.toLowerCase();
if (term.equals("terminal") || term.equals("term"))
{
filename = "terminal";
isTerminal = true;
}
}
StreamHelper streamHelper = OSResourceManager.getStreamHelper();
if (isTerminal)
{
return streamHelper.openTerminalStream();
}
else
{
// casting works only if streams are not using rpc
StreamDaemon sd = (StreamDaemon) streamHelper;
return sd.store(new FileStream(filename,
write,
append,
true,
null,
hasDuplicatedLastLine));
}
}
/**
* Constructs a remote stream instance representing the OsDirStream result of the INPUT
* THROUGH OS-DIR (dir-name) statement.
* <p>
* This will take the directory and export the list of files, directory and device names
* and properties.
*
* @param dir
* The <code>String</code> path to the directory on which OS-DIR will be applied.
* @param noAttrList
* <code>true</code> if property indexes will be added to the results.
*
* @return The stream id which is used to route operations to the newly created instance.
*/
@Override
public int openDirStream(String dir, boolean noAttrList)
{
dir = resolvePathName(dir, true);
// casting works only if streams are not using rpc
StreamDaemon sd = (StreamDaemon) OSResourceManager.getStreamHelper();
return sd.store(new DirStream(dir, noAttrList, null));
}
/**
* Check if the file system is case-sensitive
*
* @return <code>true</code> if the file system is case-sensitive
*/
@Override
public boolean isCaseSensitive()
{
return caseSens;
}
/**
* Search the current directory (and PROPATH if it is configured) for a given filename
* with the specified extensions.
*
* @param filename
* The file name.
* @param caseSensitiveExts
* The extensions to be checked if the file system is case-sensitive.
* @param caseInsensitiveExts
* The extensions to be checked if the file system is case-insensitive.
*
* @return filename or <code>null</code> if not found
*/
@Override
public String searchPathWithExts(String filename,
List<String> caseSensitiveExts,
List<String> caseInsensitiveExts)
{
// make sure there is something to search for (the code below needs to be protected)
if (filename == null)
{
return null;
}
// if the the OS is windows replace all / to \ from the search paths
String fn = normalizeFileSeparators(filename);
String result = Utils.tryWhileNull(caseSens ? caseSensitiveExts : caseInsensitiveExts,
ext -> getAbsoluteOrCurrent(caseSens, fn + ext, false));
if (result != null)
{
return result;
}
// not found in root or current, try PROPATH entries
if (paths == null)
{
return null;
}
return Utils.tryWhileNull(Arrays.asList(paths),
path -> Utils.tryWhileNull(caseSens ? caseSensitiveExts : caseInsensitiveExts,
ext -> getInPath(caseSens, path, fn + ext, false)) );
}
/**
* Access file system details regarding a specific file.
*
* @param name
* The relative or absolute filename for which to obtain
* information.
*
* @return The file details if the file was found and can be read or
* default values otherwise.
*/
public FileInfo accessFileInfo(String name)
{
FileInfo info = new FileInfo();
name = resolvePath(name);
info.pathname = search(paths, caseSens, name, true);
// did we find a match?
if (info.pathname != null)
{
// initialize the rest of our data
File local = new File(info.pathname);
try
{
FileSystemDaemon inst = instance.get();
FileSystemResource fsr = inst.fsRes;
boolean isServerSideFS = inst.isServerSideFS;
String canonPath = local.getCanonicalPath();
if (!isServerSideFS || fsr.canRead(canonPath))
{
info.size = local.length();
info.fullname = canonPath;
info.modification = local.lastModified();
info.creation = FileChecker.getFileCreationTime(info.fullname);
}
String primeType = FileChecker.getFileType(info.fullname);
StringBuilder sb = new StringBuilder(primeType);
if ((!isServerSideFS || fsr.canRead(info.fullname)) && local.canRead())
sb.append("R");
try
{
if ((!isServerSideFS || fsr.canWrite(info.fullname)) && local.canWrite())
sb.append("W");
}
catch (SecurityException se)
{
// it is OK to not have write access, don't bomb out
}
info.type = sb.toString();
}
catch (Exception exc)
{
// we probably aren't allowed to read the file, but there may
// have been some other failure, reset everything to unknown
info = new FileInfo();
}
}
// the name is always set to the unchanged input value, even if it is
// invalid
info.name = name;
return info;
}
/**
* Get the comma-separated list of filesystem roots (drive letters on
* platforms that support the concept, otherwise returns the equivalent
* of the empty string).
*
* @return The comma-separated list of drive letters or the empty string
* if the platform does not support the concept.
*/
public String getRootList()
throws IOException
{
StringBuilder sb = new StringBuilder("");
File[] drives = File.listRoots();
for (int i = 0; i < drives.length; i++)
{
// if this is the 2nd or later iteration, add a comma separator
if (i > 0)
{
sb.append(",");
}
String name = drives[i].getCanonicalPath();
int idx = name.lastIndexOf(File.separator);
// add the path
sb.append( idx < 0 ? name : name.substring(0, idx) );
}
return sb.toString();
}
/**
* Get the system property (user-defined properties can be specified
* at the Java command line) associated with the passed key.
*
* @return The system property found or <code>null</code> if the
* property does not exist.
*/
public String getProperty(String key)
{
if (key == null || key.length() == 0)
{
return null;
}
// first check the OS for environment variable value
String res = getEnvironmentValue(key);
return res != null ? res : System.getProperty(key);
}
/**
* Get the file separator for the system.
*
* @return The file separator.
*/
public String getFileSeparator()
{
return File.separator;
}
/**
* Get the path separator for the system.
*
* @return The path separator.
*/
public String getPathSeparator()
{
return File.pathSeparator;
}
/**
* Creates each of the directories requested, if it doesn't exist and all
* path segments up to the last segment do exist. If the directory name
* already exists in the filesystem (even if it is a file), this method
* just returns (this is the Progress behavior). If any error occurs, no
* exception will be thrown, but no subsequent directories in the list
* will be created.
*
* @param dirs
* The list of directories to create.
*
* @return The error code (<code>ERR_NO_ERROR</code> if no problems
* occur).
*/
public int mkdir(String[] dirs)
{
try
{
for (int i = 0; i < dirs.length; i++)
{
// safety first, should not happen
if (dirs[i] == null)
{
// but if it does, return now
return ERR_UNKNOWN;
}
dirs[i] = resolvePath(dirs[i]);
String normalizedPath = normalizeFileSeparators(dirs[i]);
File target = new File(normalizedPath);
if (target.exists() || target.getPath().isEmpty())
{
// nothing to do (Progress DOES NOT error even when the target is a FILE!)
continue;
}
File parent = target.getParentFile();
// is a parent dir specified? (if it is relative, it is not)
if (parent != null)
{
// the parent needs to exist
if (!parent.exists())
{
boolean found = false;
if (!caseSens)
{
String parentName = getCaseInsensitiveMatch(parent, true);
found = parentName != null;
if (found)
{
if (normalizedPath.endsWith(File.separator))
{
normalizedPath = normalizedPath.substring(0, normalizedPath.length() - 1);
}
int index = normalizedPath.lastIndexOf(File.separator);
if (index != -1)
{
String targetDir = normalizedPath.substring(index + 1);
parent = new File(parentName);
target = new File(parent, targetDir);
if (target.exists())
{
continue;
}
}
}
}
if (!found)
{
return ERR_NO_SUCH_FILE_OR_DIR;
}
}
// the parent needs to be a directory
if (!parent.isDirectory())
{
return ERR_NOT_DIR;
}
}
try
{
if (isServerSideFS && !fsRes.canCreate(target.getCanonicalPath()))
{
return ERR_PERM_DENIED;
}
}
catch (IOException e)
{
// this should not happen
return ERR_IO;
}
// make the directory
boolean result = target.mkdir();
// did a failure occur?
if (!result)
{
// unknown error
return ERR_UNKNOWN;
}
}
}
catch (SecurityException se)
{
return ERR_PERM_DENIED;
}
return ERR_NO_ERROR;
}
/**
* Renames (and moves if the path is different) the specified source file
* to the target filename (and path). If any error occurs, no
* exception will be thrown.
*
* @param source
* The name of the source file or directory.
* @param target
* The name of the target file or directory. If this is a
* directory name for a directory that already exists and the
* <code>source</code> is a file, then the source file name will
* be used as the target file in the directory given.
*
* @return The error code (<code>ERR_NO_ERROR</code> if no problems
* occur).
*/
public int rename(String source, String target)
{
if (source == null || target == null)
{
return ERR_NO_ERROR;
}
source = resolvePath(source);
target = resolvePath(target);
File src = new File(normalizeFileSeparators(source));
File tar = new File(normalizeFileSeparators(target));
try
{
// can't rename a non-existing source file or directory
if (!src.exists())
{
return ERR_NO_SUCH_FILE_OR_DIR;
}
if (tar.exists())
{
if (tar.isDirectory())
{
// append the source file or directory name to the target path
tar = repathSource(src, target);
}
else
{
return ERR_FILE_EXISTS;
}
}
if (isServerSideFS)
{
try
{
String srcPath = src.getCanonicalPath();
String tarPath = tar.getCanonicalPath();
if (!fsRes.canDelete(srcPath) || !fsRes.canCreate(tarPath))
{
return ERR_PERM_DENIED;
}
}
catch (IOException e)
{
return ERR_IO;
}
}
// try the rename operation
boolean result = src.renameTo(tar);
// did a failure occur?
if (!result)
{
// unknown error
return ERR_UNKNOWN;
}
}
catch (SecurityException se)
{
return ERR_PERM_DENIED;
}
return ERR_NO_ERROR;
}
/**
* Initializes the path for subsequent searches by {@link #searchPath}.
*
* @param paths
* The list of paths to search for files specified by relative
* filename.
* @param caseSens
* <code>true</code> if the legacy system had a case-sensitive
* file-system.
*/
public void setSearchPath(String[] paths, boolean caseSens)
{
this.caseSens = caseSens;
this.paths = fixPathNames(paths);
}
/**
* Gets a copy of its search path.
*
* @return The list backed by the search path array copy.
*/
@Override
public List<String> getSearchPath()
{
if (paths != null && paths.length > 0)
{
return Arrays.asList(Arrays.copyOf(paths, paths.length));
}
else
{
return Collections.emptyList();
}
}
/**
* Search for the given file in each directory listed in the path
* previously specified via {@link #setSearchPath}. If the file system
* object exists and is a file, then the filename is returned. If the path
* contains an empty string or a '.', then the current directory is
* searched, otherwise only the listed directories are searched.
* <p>
* If the filename is specified as an absolute name, this specific
* filename is checked and no path searching is done.
*
* @param filename
* The absolute or relative filename for which to search.
*
* @return If the file is found in the current directory, the relative
* filename is returned. If the file is absolute or found in any
* other path, the full absolute filename is returned. If the
* file is not found or the referenced file system object is not
* a file, <code>null</code> is returned.
*/
public String searchPath(String filename)
{
boolean beginsWithDot = filename.startsWith(".");
// the resolved file name can begin with "."
filename = resolvePath(filename);
return search(paths, caseSens, filename, false, beginsWithDot);
}
/**
* Copies (or appends) the specified source file to or over the target
* file. The mode (copy/overwrite or append) is determined by the
* <code>overwrite</code> parameter. If any error occurs, no exception
* will be thrown.
*
* @param source
* The name of the source file. Must NOT be a directory name.
* Must be an existing file.
* @param target
* The name of the target file. If this is a directory name,
* the source file name will be used as the target file in
* the directory given.
* @param overwrite
* If <code>true</code> this is a copy, otherwise it is an
* append.
*
* @return The error code (<code>ERR_NO_ERROR</code> if no problems
* occur).
*/
public int copy(String source, String target, boolean overwrite)
{
source = resolvePath(source);
target = resolvePath(target);
source = normalizeFileSeparators(source);
target = normalizeFileSeparators(target);
File src = new File(source);
File tar = new File(target);
try
{
// make sure the source file exists
if (!src.exists())
{
return ERR_NO_SUCH_FILE_OR_DIR;
}
// the source file can't be a directory
if (src.isDirectory())
{
return ERR_IS_DIR;
}
String srcPath;
try
{
srcPath = src.getCanonicalPath();
}
catch (IOException e)
{
return ERR_IO;
}
// we need read access to the source
if (isServerSideFS && !fsRes.canRead(srcPath) || !src.canRead())
{
return ERR_PERM_DENIED;
}
// preprocess the target file
if (tar.exists())
{
if (tar.isDirectory())
{
// switch to overwrite mode if not already in that mode
overwrite = true;
// append the source file name to the target path
tar = repathSource(src, target);
target = tar.getAbsolutePath();
}
// re-check because directory processing may have reset the
// target to a file that doesn't exist
if (tar.exists())
{
if (tar.isFile())
{
String tarCanPath;
try
{
tarCanPath = tar.getCanonicalPath();
}
catch (IOException e)
{
return ERR_IO;
}
// we need write access to the target (this can only be
// called if the file exists)
if (isServerSideFS && !fsRes.canWrite(tarCanPath) || !tar.canWrite())
{
return ERR_PERM_DENIED;
}
if (overwrite)
{
// delete the target file
tar.delete();
}
}
else if (tar.isDirectory())
{
// something is wrong and it can't be corrected
return ERR_IS_DIR;
}
}
}
else
{
// switch to overwrite mode if not already in that mode, since
// there is no target file
overwrite = true;
}
if (isServerSideFS)
{
try
{
String tarPath = tar.getCanonicalPath();
if (!tar.exists() && !fsRes.canCreate(tarPath))
{
return ERR_PERM_DENIED;
}
if (!fsRes.canWrite(tarPath))
{
return ERR_PERM_DENIED;
}
}
catch (IOException e)
{
return ERR_IO;
}
}
// best efforts attempt to copy
boolean result = Utils.copyFile(source, target, 0, !overwrite);
// did a failure occur?
if (!result)
{
// on any failure, remove partial results
if (tar.exists())
{
tar.delete();
}
// unknown error
return ERR_UNKNOWN;
}
}
catch (SecurityException se)
{
return ERR_PERM_DENIED;
}
return ERR_NO_ERROR;
}
/**
* Deletes the specified file system entity, if necessary using recursion
* to delete all contents (if it is a directory) first. If any error
* occurs, no exception will be thrown.
*
* @param target
* The target file or directory to delete.
* @param recursive
* Recursively decend through all subdirectories, deleting
* the entire subtree.
*
* @return The error code (<code>ERR_NO_ERROR</code> if no problems
* occur).
*/
public int delete(String target, boolean recursive)
{
// safety first, should not happen
if (target == null)
{
// but if it does, return now
return ERR_UNKNOWN;
}
target = resolvePath(target);
return deleteWorker(new File(normalizeFileSeparators(target)), recursive);
}
/**
* Deletes each of the directories or files specified. If a non-empty
* directory name is specified, an error will occur unless the
* <code>recursive</code> flag is <code>true</code>. If any error occurs,
* no exception will be thrown, the deletion process just continues. The
* last error will be the one returned even if there were prior successful
* deletes or other failures.
*
* @param list
* The list of files and directories to delete.
* @param recursive
* Recursively decend through all subdirectories, deleting
* the entire subtree.
*
* @return The error code (<code>ERR_NO_ERROR</code> if no problems
* occur).
*/
public int delete(String[] list, boolean recursive)
{
// set our result code, on error we keep going and the last error is
// returned, even if some of the deletes succeed (for this reason, if
// we set the error code at the beginning, then only if everything
// succeeds will the rc be ERR_NO_ERROR, otherwise the last rc will
// be accessible --> this is just as Progress does it)
int rc = ERR_NO_ERROR;
for (int i = 0; i < list.length; i++)
{
// safety first, should not happen
if (list[i] == null)
{
// but if it does, return now
return ERR_UNKNOWN;
}
list[i] = resolvePath(list[i]);
rc = deleteWorker(new File(normalizeFileSeparators(list[i])), recursive);
}
return rc;
}
/**
* Get the file contents from the local file system.
*
* @param filename
* Name of the file in the client-side file system (relatively to the current
* directory on the client side).
*
* @return The file's contents as an array of bytes.
*
* @throws java.io.IOException
* If the file cannot not be found or read, or if it is too large.
*/
@Override
public byte[] getFileContents(String filename)
throws IOException
{
// safety first, should not happen
if (filename == null)
{
// but if it does, return now
throw new FileNotFoundException("Filename must not be null.");
}
filename = resolvePath(filename);
File file = new File(normalizeFileSeparators(filename));
if (!file.exists())
{
throw new FileNotFoundException("File " + filename + " does not exist.");
}
else if (file.isDirectory())
{
throw new FileNotFoundException("File " + filename + " is a directory.");
}
else if (file.length() > MAXIMUM_FILE_TRANSFER_SIZE)
{
throw new FileNotFoundException("File " + filename + " is too large.");
}
String path = file.getCanonicalPath();
if (isServerSideFS && !fsRes.canRead(path))
{
throw new IOException("Permission denied.");
}
return Files.readAllBytes(file.toPath());
}
/**
* Sets the legacy file system parameters to be used by the client.
*
* @param fileSeparator
* The legacy file separator
* @param caseSensitive
* The legacy case sensitive mode
* @param pathMap
* The legacy path map
* @param paths
* The list of paths to search for files specified by relative
* filename.
* @param areLegacyPathsNotResolved
* The true value of this parameter indicates that legacy path names must be resolved
* on the host system, and false value means that legacy path names are not required
* to be resolved.
*/
@Override
public void setLegacyFileSystemParameters(String fileSeparator,
boolean caseSensitive,
Map<String, String> pathMap,
String[] paths,
boolean areLegacyPathsNotResolved)
{
if (areLegacyPathsNotResolved)
{
pathResolver = Utils.getSafePathResolver(fileSeparator, caseSensitive, pathMap);
}
this.caseSens = caseSensitive;
this.paths = fixPathNames(paths);
}
/**
* Resolves the given path by the underlined path resolver, otherwise returns the given
* unresolved value.
*
* @param path
* The given path to be resolved
*/
@Override
public String resolvePath(String path)
{
if (pathResolver == null)
{
return path;
}
return pathResolver.resolvePath(path);
}
/**
* Gets the working directory.
*
* @return The current working directory.
*/
public String getWorkingDirectory()
{
return getWorkingDir();
}
/**
* Sets the working directory via native OS call.
*
* @param newWorkingDir
* A new working directory to set.
*/
public void setWorkingDirectory(String newWorkingDir)
{
if (setWorkingDir(newWorkingDir))
{
// refresh Java property if directory was really changed
System.setProperty("user.dir", newWorkingDir);
}
}
/**
* Gets the name of the directory where temporary files are stored.
* Corresponds with the <code>SESSION:TEMP-DIRECTORY</code> system handle attribute.
*
* @return Name of temporary directory.
*/
@Override
public String getTempDirectory()
{
return Utils.getOrCreateDefaultFwdTemporaryDirectory();
}
/**
* Resolves the given legacy path names on the host system and updates them accordingly.
*
* @param pathNames
* The given legacy path names
*
* @return The array of resolved path names.
*/
private String[] fixPathNames(String[] pathNames)
{
int size = pathNames != null ? pathNames.length : 0;
for(int i = 0; i < size; i++)
{
pathNames[i] = resolvePath(pathNames[i]);
}
return pathNames;
}
/**
* Takes the last part of the source filename (removes the path) and
* appends this to the target directory name.
*
* @param source
* The source file or directory. No call to {@link #normalizeFileSeparators}
* will be made. It is assumed this is already normalized.
* @param target
* The target directory (cannot be a file). No call to
* {@link #normalizeFileSeparators} will be made. It is assumed this is already
* normalized.
*
* @return The object representing the resulting file name.
*/
private File repathSource(File source, String target)
{
StringBuilder sb = new StringBuilder(target);
// append the path segment separator if needed
if (!target.endsWith(File.separator))
{
sb.append(File.separator);
}
// append the source file/directory name (remove the path)
sb.append(source.getName());
// reset the target
target = sb.toString();
return new File(target);
}
/**
* Tries to resolve the given legacy path name into its host path name, fix it according to
* the environment file separator and applies case insensitive search if the legacy file system
* is case insensitive.
*
* @param path
* The given legacy path name
* @param isDirectory
* True iff the given path name represents a directory
*
* @return The transformed path name.
*/
private String resolvePathName(String path, boolean isDirectory)
{
path = resolvePath(path);
path = FileSystemDaemon.normalizeFileSeparators(path);
if (!isCaseSensitive())
{
File targetFile = new File(path);
if (targetFile.exists())
{
if ((targetFile.isFile() && !isDirectory) || (targetFile.isDirectory() && isDirectory))
{
//return path
return targetFile.getPath();
}
}
String matched = FileSystemDaemon.getCaseInsensitiveMatch(targetFile, isDirectory);
if (matched != null)
{
return matched;
}
}
return path;
}
}