存储大量用户数据的最佳方法

12
我将用户文件存储在他们自己的名称目录中,类似于以下内容:
/username/file01.jpg
/username/file02.mp4
/username/file03.mp3

但是,如果有更多的用户上传更多的文件,就会出现问题,因为这将导致一些或许很多用户迁移到另一个驱动器。我首先选择用户名目录解决方案,因为我不想让文件名混在一起。我也不想改变文件名。另外,如果另一个用户上传相同的文件名,则会创建问题,如果文件以原始名称存储。

什么是最好的方法呢?我有一个解决方案,但想问问社区是否这是最好的方法。

我将使用连续的文件夹,然后将文件名哈希成非常独特的东西并存储到目录中。 我要做的是将文件的原始名称和用户名存储到数据库中,并存储在磁盘上的文件名哈希值。

当任何人想要访问该文件时,我将通过php读取该文件,替换名称或在那个点上执行某些操作,以便该文件被下载为原始文件名。

我只有这个提议的解决方案。 你们有比这个更好的吗?

编辑:

我也使用文件夹系统,并且可能会使用虚拟文件夹进行第二种方式。我的数据库是MongoDB

你们所有人的答案都很棒,真的很有帮助。 我想给每个人奖励,这就是我留下它的原因,以便社区可以自动提供。感谢大家的回答,我真的很感激。


3
我发现用户ID(不变的值)更适合组织上传文件。虽然手动导航较为困难(无法通过查看文件夹来确定上传者身份),但这样可以让用户名更改而不会破坏对应的资产文件夹。 - Jason Sperske
7
因为您正在为每个文件创建一个数据库条目,所以您可以存储一个“存储卷 ID”,每次在存储卷上用尽空间时就会增加该值。当获取文件时,您将获得用户 ID、文件哈希和存储卷名称,这些信息可以组合使用以检索资源。我只是使用了亚马逊 S3,并让他们处理这样的事情。 - Jason Sperske
1
你也可以考虑使用像AWS S3这样的基于云的解决方案,它会自动处理扩展问题。我们使用类似的结构(带有ID)来管理S3上的用户文件。 - Wandering Digital
1
我的下载解决方案之一是将所有内容保存在公共目录之外的文件夹中。上传时,它会被保存到数据库中并带有路径,例如:/home/user/files/1/image.png,然后我会根据文件名创建哈希并插入ID,然后将其保存到数据库中。检索只需使用PHP即可控制下载和下载计数器。任何具有相同文件名的内容都没有关系。并且一定要研究S3。 - Dave
1
我看到的唯一没有被建议的事情是将文件存储在从文件哈希生成的路径/名称下(sha1或更长的哈希,不太可能生成冲突并且很可能在哈希之间具有相当均匀的分布)。这里的好处是通过数据库,可以将生成相同哈希值的文件(理想情况下是多次上传的相同文件)链接到多个用户。只是好奇为什么没有建议这样做的原因。 - Jonathan Kuhn
显示剩余6条评论
10个回答

9
你能创建关系型MySQL表吗?例如:
一个名为“users”的表和一个名为“files”的表。
你的“users”表将跟踪所有你已经在追踪的内容:
id、name、email等。
然后,“files”表将存储类似以下内容:
id、fileExtension、fileSize、userID <---- userID将成为指向“files”表中id字段的外键。
那么,当你保存文件时,你可以将它保存为它的id.fileExtension,并使用查询来提取与该文件相关联的用户,或者提取与某个用户相关联的所有文件。
例如:
SELECT users.name, files.id, files.extension
FROM `users`
INNER JOIN `files` on users.id = files.userID;

1
嗨,我不使用MySQL,而是使用MongoDB。但这种方法也更好。这意味着您支持数据库处理所有文件细节。 - Abhishek

8
我在数据库中处理文件元数据,并使用UUID检索文件。我的工作流程包括:
  1. 基于内容的识别
    1. 从文件内容中获取MD5值
    2. 使用命名空间UUID:v5生成基于用户UUID和文件MD5的唯一标识符。
    3. 使用自定义函数根据“realname”生成路径。
    4. 在数据库中保存:uuid、originalname(上传的名称)、realname(生成的名称)、filesize和mime。(可选dateAdded和md5)
  2. 文件检索。
    1. 使用UUID检索元数据。
    2. 基于realname重新生成文件路径。
    3. 使用originalname向用户展示熟悉的文件名。
我处理文件名,将其分配为命名空间UUID作为数据库主键,并根据用户和文件名生成路径。前提条件是您的用户已分配UUID。以下代码将帮助您避免数据库中的ID冲突,并帮助您通过其内容识别文件(如果您需要一种方法来识别重复的内容而不是文件名)。
$fileInfo = pathinfo($_FILE['file']['name']);
$extension = (isset($fileInfo['extension']))?".".$fileInfo['extension']:"";

$md5Name = md5_file($_FILE['file']['tmp_name']); //you could use other hash algorithms if you are so inclined.

$realName = UUID::v5($user->uuid, $md5Name) . $extension; //UUID::v5(namespace, value).

我使用一个函数来根据一些自定义参数生成文件路径,你可以使用 $username 和 $realname。如果你实现了一个分布式的文件夹结构,你可能已经将其分区在文件命名方案或任何自定义方案上,这将非常有用。

function generateBasePath($realname, $customArgsArray){
    //Process Args as your requirements.
    //might as well be  "$FirstThreeCharsFromRealname/"
    //or a checksum that helps you decide which drive/volume/mountpoint to use.
    //like some files on the local disk and some other from an Amazon::S3 mountpoint.
    return $mountpoint.'/'.$generatedPath; 
}

作为额外的奖励,这还有以下好处:
  1. 如果在文件记录上添加一个属性,指明该文件(uuid)替代了哪个文件,它可以帮助您维护版本化文件存储库。
  2. 如果添加'owner'和/或'group'属性,则可以创建一个应用程序访问控制列表。
  3. 它也适用于单个文件夹结构。
注意:我以php的$_FILE作为文件来源的示例,基于此问题的标签。它可以来自任何文件来源或生成的内容。

5

既然您已经在使用MongoDB,我建议您看一下GridFS。它是一种规范,允许您将文件(即使它们大于16mb)存储到MongoDB集合中。

它具有可扩展性,因此如果您添加另一个服务器,就不会出现任何问题,它还存储元数据,可以分块读取文件,并且还具有内置的备份功能。


3

另一种策略是创建一个二维结构,其中第一层目录是用户名的前两个字符,然后第二层目录是剩余的字符(类似于Git存储其SHA-1对象ID的方式)。例如:

/files/jr/andomuser/456.jpg

针对用户“jrandomuser”。

请注意,因为用户名可能不会像SHA-1值一样随机分布,所以您可能需要稍后添加另一个级别。虽然这很少见。


你的想法真的很令人印象深刻。我会考虑一下。但问题仍然存在于磁盘数据存储方面。如果涉及到用户和文件夹的保留,该如何解决呢?由于Amazon S3不允许文件夹,如果他们允许,我将没有问题获取他们的存储空间,因为这样他们就需要负责管理我的文件。 - Abhishek

3
我会根据文件名、上传的日期和时间以及用户名生成一个GUID,将这些值和文件路径保存在数据库中以供以后使用。如果你生成这样的GUID,文件名就不可预测了。
举个例子,比如用户Daniel Steiner(即我)于2013年4月23日12:37am上传了一个名为resume.doc的文件到您的服务器。这将给出一个基本值:Daniel_Steiner+2013/23/04+00:37+resume.doc,然后将作为MD5哈希05c2d2f501e738b930885d991d136f1e。为确保文件能够在正确的程序中打开,我们会随后添加正确的文件扩展名,从而得到类似于http://link.to/your/site/05c2d2f501e738b930885d991d136f1e.doc这样的链接。如果您的用户帐户已经有一个用户ID,您可以将其添加到URL中。例如,如果我的用户ID是123145,则URL将为http://link.to/your/site/123145/05c2d2f501e738b930885d991d136f1e.doc
如果将原始文件名保存到数据库中,您还可以提供一个下载脚本,以原始文件名提供文件下载,即使在您的服务器上它有另一个文件名。
如果你想的话,我也可以给出一个PHP示例 - 代码不会太多。

2
我建议使用以下数据库结构:
文件表至少包含以下内容:
IDFile列是自增列/主键。 UserID是可空外键。
对于FK_File_User,我建议:
ON UPDATE NO ACTION -- IDUser is auto_increment too. No changes need to be tracked.
ON DELETE SET NULL  -- If user deleted, then File is not owned. Might be deleted
                    -- with CRON job or something else.

另外,File表可能会添加其他列:

  1. 实际上传日期和时间
  2. 实际 MIME 类型
  3. 实际存储位置(对于分布式存储系统)
  4. 下载计数(另一个表可能是更好的解决方案)

等等...

一些好处:

  1. 您不需要计算文件大小、哈希、扩展名或任何文件元数据,因为您可以通过一个数据库操作获得它。
  2. 您可以通过单个 SELECT ... GROUP BY ... WITH ROLLUP 语句为 File 表中每个用户获取文件数量/使用空间/任何您编写的统计信息,并且这比分析实际文件要快,因为实际文件可能分布在多个存储设备上。
  3. 您可以为不同的用户应用文件访问权限。这将不会显着改变表结构数据库。

我不认为原始文件名需要存储,因为有两个原因:

  1. 文件可能具有名称,该名称不受服务器操作系统文件系统正确支持,例如 Cyrillic。
  2. 两个不同的文件可能具有完全相同的名称,因此其中一个可能被另一个覆盖。

因此,有一个解决方案:

1) 在上传到 File 表的 INSERT 中将文件重命名为 IDFile。这是安全的,没有重复。

2) 在需要/下载文件时恢复文件名,例如:

// peform query to "File" table by given ID

list($name, $ext, $size, $md5) = $result->fetch_row();

$result->free();

header('Content-Length: ' . $size);
header('Content-MD5: ' . $md5);
header('Accept-Ranges: bytes');
header('Connection: close');
header('Content-Type: application/force-download');
header('Content-Disposition: attachment; filename="' . $name . '.' . $ext . '"');

// flush file content

3)实际文件可以存储在单个目录中(因为 IDFile 是安全的),也可以存储在以 IDUser 命名的子目录中,这取决于具体情况。

4)由于 IDFile 是直接序列,如果某些文件丢失了,您可以通过评估实际文件名序列中缺失的段来获取它们的数据库元数据。然后,您可以执行“通知所有者”、“删除文件元数据”或这两个操作。


我反对将大型实际文件作为二进制内容存储在 DBMS 中的想法。

DBMS 是关于数据和分析的,它不是一个文件系统,如果我的看法有用的话,就不应该以这种方式使用它。


2
看起来跟我的方法差不多;)而且,我也反对将二进制文件存储在数据库中! - Daniel Steiner

2
由于文件系统是一棵树,而不是一个图形(面向分类),因此很难找到一种方法来轻松表示多个实体,例如用户、媒体类型、日期、事件、图像裁剪类型等。这就是为什么使用关系型数据库更容易 - 它可以转换为图形。
但由于它是另一个抽象级别,您需要编写执行低级同步的函数,包括避免名称冲突、长路径名、每个文件夹的大文件数量、每个实体的传输易用性、水平扩展等。因此,这取决于您的应用程序需要多复杂。

1
你可以安装LDAP服务器。LDAP查找非常快,因为它针对大量读取操作进行了高度优化。你甚至可以查询数据。
LDAP以树形结构组织数据。
你可以按照以下示例组织数据“用户-> IP地址->文件夹->文件名”。这样文件可以在物理/地理上分散,并且你可以非常快速地获取位置。
你也可以使用标准的LDAP查询进行查询,例如获取特定用户的所有文件列表或获取文件夹中的文件列表等。

0
  1. 使用Mongodb存储实际文件名(例如:myImage.jpg)和其他属性(例如:MIME类型),以及来自步骤2和3的$random-text.jpg

  2. 生成一些$random-text,例如:base_convert(mt_rand(), 10, 36)uniqid($username, true);

  3. 将文件物理存储为$random-text.jpg - 始终保持相同的扩展名是一个好习惯

  4. 注意:使用filter_var()确保输入的文件名不会对Mongodb造成安全风险。

亚马逊S3可靠且便宜,但要注意S3的“最终并发性”。


0
假设用户在数据库中有唯一的ID(主键),如果ID为73的用户上传文件,则保存如下:

"uploads/$userid_$filename.$ext"

例如,73_resume.doc,73_myphoto.jpg。
现在,在获取文件时,请使用此代码:
foreach (glob("uploads/$userid_*.*") as $filename) {
    echo $filename;
}

这可以与散列解决方案(存储在数据库中)相结合,以便用户在浏览器地址栏中不会随机尝试74_photo.jpg,而得到的下载路径是73_photo.jpg。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接