我试图找出将用户上传的文件存储在文件系统中的最佳方式。文件范围从个人文件到 wiki 文件。当然,数据库将以某种我尚未弄清楚的方式指向这些文件。
基本要求:
- 童话般的安全,所以人们无法猜测文件名(Picture001.jpg、Picture002.jpg、Music001.mp3 是一个很大的禁忌)
- 轻松备份和镜像(我更喜欢一种方式,这样我就不必每次备份时都复制整个 HDD。我喜欢只备份最新项目的想法,但我对这里的选项很灵活。 )
- 如果需要,可扩展到多台服务器上的数百万个文件。
我试图找出将用户上传的文件存储在文件系统中的最佳方式。文件范围从个人文件到 wiki 文件。当然,数据库将以某种我尚未弄清楚的方式指向这些文件。
基本要求:
一种技术是将数据存储在以其内容的哈希 (SHA1) 命名的文件中。这不容易猜到,任何备份程序都应该能够处理它,并且它很容易分片(通过在一台机器上存储以 0 开头的哈希值,在下一台机器上存储以 1 开头的哈希值,等等)。
数据库将包含用户分配的名称和内容的 SHA1 哈希之间的映射。
文件名指南,自动扩展文件夹层次结构,每个文件夹中不超过几千个文件/文件夹。备份新文件是通过备份新文件夹来完成的。
您没有说明您正在使用什么环境和/或编程语言,但这里有一个 C#/.net/Windows 示例:
using System;
using System.IO;
using System.Xml.Serialization;
/// <summary>
/// Class for generating storage structure and file names for document storage.
/// Copyright (c) 2008, Huagati Systems Co.,Ltd.
/// </summary>
public class DocumentStorage
{
private static StorageDirectory _StorageDirectory = null;
public static string GetNewUNCPath()
{
string storageDirectory = GetStorageDirectory();
if (!storageDirectory.EndsWith("\\"))
{
storageDirectory += "\\";
}
return storageDirectory + GuidEx.NewSeqGuid().ToString() + ".data";
}
public static void SaveDocumentInfo(string documentPath, Document documentInfo)
{
//the filestream object don't like NTFS streams so this is disabled for now...
return;
//stores a document object in a separate "docinfo" stream attached to the file it belongs to
//XmlSerializer ser = new XmlSerializer(typeof(Document));
//string infoStream = documentPath + ":docinfo";
//FileStream fs = new FileStream(infoStream, FileMode.Create);
//ser.Serialize(fs, documentInfo);
//fs.Flush();
//fs.Close();
}
private static string GetStorageDirectory()
{
string storageRoot = ConfigSettings.DocumentStorageRoot;
if (!storageRoot.EndsWith("\\"))
{
storageRoot += "\\";
}
//get storage directory if not set
if (_StorageDirectory == null)
{
_StorageDirectory = new StorageDirectory();
lock (_StorageDirectory)
{
string path = ConfigSettings.ReadSettingString("CurrentDocumentStoragePath");
if (path == null)
{
//no storage tree created yet, create first set of subfolders
path = CreateStorageDirectory(storageRoot, 1);
_StorageDirectory.FullPath = path.Substring(storageRoot.Length);
ConfigSettings.WriteSettingString("CurrentDocumentStoragePath", _StorageDirectory.FullPath);
}
else
{
_StorageDirectory.FullPath = path;
}
}
}
int fileCount = (new DirectoryInfo(storageRoot + _StorageDirectory.FullPath)).GetFiles().Length;
if (fileCount > ConfigSettings.FolderContentLimitFiles)
{
//if the directory has exceeded number of files per directory, create a new one...
lock (_StorageDirectory)
{
string path = GetNewStorageFolder(storageRoot + _StorageDirectory.FullPath, ConfigSettings.DocumentStorageDepth);
_StorageDirectory.FullPath = path.Substring(storageRoot.Length);
ConfigSettings.WriteSettingString("CurrentDocumentStoragePath", _StorageDirectory.FullPath);
}
}
return storageRoot + _StorageDirectory.FullPath;
}
private static string GetNewStorageFolder(string currentPath, int currentDepth)
{
string parentFolder = currentPath.Substring(0, currentPath.LastIndexOf("\\"));
int parentFolderFolderCount = (new DirectoryInfo(parentFolder)).GetDirectories().Length;
if (parentFolderFolderCount < ConfigSettings.FolderContentLimitFolders)
{
return CreateStorageDirectory(parentFolder, currentDepth);
}
else
{
return GetNewStorageFolder(parentFolder, currentDepth - 1);
}
}
private static string CreateStorageDirectory(string currentDir, int currentDepth)
{
string storageDirectory = null;
string directoryName = GuidEx.NewSeqGuid().ToString();
if (!currentDir.EndsWith("\\"))
{
currentDir += "\\";
}
Directory.CreateDirectory(currentDir + directoryName);
if (currentDepth < ConfigSettings.DocumentStorageDepth)
{
storageDirectory = CreateStorageDirectory(currentDir + directoryName, currentDepth + 1);
}
else
{
storageDirectory = currentDir + directoryName;
}
return storageDirectory;
}
private class StorageDirectory
{
public string DirectoryName { get; set; }
public StorageDirectory ParentDirectory { get; set; }
public string FullPath
{
get
{
if (ParentDirectory != null)
{
return ParentDirectory.FullPath + "\\" + DirectoryName;
}
else
{
return DirectoryName;
}
}
set
{
if (value.Contains("\\"))
{
DirectoryName = value.Substring(value.LastIndexOf("\\") + 1);
ParentDirectory = new StorageDirectory { FullPath = value.Substring(0, value.LastIndexOf("\\")) };
}
else
{
DirectoryName = value;
}
}
}
}
}
文件名的 SHA1 哈希 + 盐(或者,如果需要,文件内容的哈希值。这使得检测重复文件更容易,但也给服务器带来了更多压力)。这可能需要一些调整才能使其独一无二(即添加上传的用户 ID 或时间戳),并且加盐是为了使其不可猜测。
文件夹结构然后由散列的部分组成。
例如,如果哈希是“2fd4e1c67a2d28fced849ee1bb76e7391b93eb12”,那么文件夹可能是:
/2
/2/2f/
/2/2f/2fd/
/2/2f/2fd/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
这是为了防止大文件夹(某些操作系统无法枚举包含一百万个文件的文件夹,因此为部分哈希创建了几个子文件夹。多少级别?这取决于您期望的文件数量,但通常是 2 或 3合理的。
就您问题的一个方面(安全性)而言:将上传的文件安全地存储在文件系统中的最佳方法是确保上传的文件不在 webroot 之外(即,您不能通过 URL 直接访问它们 - 您必须通过脚本)。
这使您可以完全控制人们可以下载的内容(安全性)并允许进行诸如日志记录之类的事情。当然,您必须确保脚本本身是安全的,但这意味着只有您允许的人才能下载某些文件。
扩展 Phill Sacre 的回答,安全性的另一个方面是为上传的文件使用单独的域名(对于即时,维基百科使用 upload.wikimedia.org),并确保该域无法读取您网站的任何 cookie。这可以防止人们上传带有脚本的 HTML 文件来窃取用户的会话 cookie(仅设置 Content-Type 标头是不够的,因为已知某些浏览器会忽略它并根据文件的内容进行猜测;它还可以嵌入到其他类型的文件中,因此检查 HTML 并禁止它并非易事)。