我有一个REST web服务,目前公开这个URL:

http://server/data/media

用户可以POST以下JSON:

{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873
}

来创建一个新的媒体元数据。

现在我需要能够在上传媒体元数据的同时上传文件。最好的办法是什么?我可以引入一个名为file的新属性,并用base64对文件进行编码,但我想知道是否有更好的方法。

还有使用多部分/表单数据,就像HTML表单会发送的那样,但我使用的是REST web服务,如果可能的话,我想坚持使用JSON。


当前回答

基于ccleve的回答,如果你正在使用superagent / express / multer,在前端构建你的多部分请求,这样做:

superagent
    .post(url)
    .accept('application/json')
    .field('myVeryRelevantJsonData', JSON.stringify({ peep: 'Peep Peep!!!' }))
    .attach('myFile', file);

cf https://visionmedia.github.io/superagent/ multipart-requests。

在快速端,作为字段传递的任何内容都将以req结束。做后的身体:

app.use(express.json({ limit: '3MB' }));

你的路线应该包括这样的内容:

const multerMemStorage = multer.memoryStorage();
const multerUploadToMem = multer({
  storage: multerMemStorage,
  // Also specify fileFilter, limits...
});

router.post('/myUploads',
  multerUploadToMem.single('myFile'),
  async (req, res, next) => {
    // Find back myVeryRelevantJsonData :
    logger.verbose(`Uploaded req.body=${JSON.stringify(req.body)}`);

    // If your file is text:
    const newFileText = req.file.buffer.toString();
    logger.verbose(`Uploaded text=${newFileText}`);
    return next();
  },
  ...

但是有一件事要记住,这是来自multer文档的关于磁盘存储的说明:

注意req。尸体可能还没被填满。这取决于客户端向服务器传输字段和文件的顺序。

我猜这意味着它将是不可靠的,比如说,计算基于json元数据的目标dir/filename沿文件传递

其他回答

仅仅因为你没有用JSON包装整个请求体,并不意味着使用multipart/form-data在一个请求中同时发布JSON和文件就不是RESTful的:

curl -F "metadata=<metadata.json" -F "file=@my-file.tar.gz" http://example.com/add-file

服务器端:

class AddFileResource(Resource):
    def render_POST(self, request):
        metadata = json.loads(request.args['metadata'][0])
        file_body = request.args['file'][0]
        ...

要上传多个文件,可以为每个文件使用单独的“表单字段”:

curl -F "metadata=<metadata.json" -F "file1=@some-file.tar.gz" -F "file2=@some-other-file.tar.gz" http://example.com/add-file

...在这种情况下,服务器代码将有请求。Args ['file1'][0]和request.args['file2'][0]

或者多次重复使用同一个:

curl -F "metadata=<metadata.json" -F "files=@some-file.tar.gz" -F "files=@some-other-file.tar.gz" http://example.com/add-file

...在这种情况下请求。Args ['files']将只是一个长度为2的列表。

或者通过一个字段传递多个文件:

curl -F "metadata=<metadata.json" -F "files=@some-file.tar.gz,some-other-file.tar.gz" http://example.com/add-file

...在这种情况下请求。args['files']将是一个包含所有文件的字符串,你必须自己解析——不知道怎么做,但我相信这并不难,或者最好使用前面的方法。

@和<之间的区别在于,@导致文件作为文件上传附加,而<将文件内容作为文本字段附加。

附注:仅仅因为我使用curl作为一种生成POST请求的方式,并不意味着完全相同的HTTP请求不能从编程语言(如Python)或使用任何足够强大的工具发送。

基于ccleve的回答,如果你正在使用superagent / express / multer,在前端构建你的多部分请求,这样做:

superagent
    .post(url)
    .accept('application/json')
    .field('myVeryRelevantJsonData', JSON.stringify({ peep: 'Peep Peep!!!' }))
    .attach('myFile', file);

cf https://visionmedia.github.io/superagent/ multipart-requests。

在快速端,作为字段传递的任何内容都将以req结束。做后的身体:

app.use(express.json({ limit: '3MB' }));

你的路线应该包括这样的内容:

const multerMemStorage = multer.memoryStorage();
const multerUploadToMem = multer({
  storage: multerMemStorage,
  // Also specify fileFilter, limits...
});

router.post('/myUploads',
  multerUploadToMem.single('myFile'),
  async (req, res, next) => {
    // Find back myVeryRelevantJsonData :
    logger.verbose(`Uploaded req.body=${JSON.stringify(req.body)}`);

    // If your file is text:
    const newFileText = req.file.buffer.toString();
    logger.verbose(`Uploaded text=${newFileText}`);
    return next();
  },
  ...

但是有一件事要记住,这是来自multer文档的关于磁盘存储的说明:

注意req。尸体可能还没被填满。这取决于客户端向服务器传输字段和文件的顺序。

我猜这意味着它将是不可靠的,比如说,计算基于json元数据的目标dir/filename沿文件传递

我同意Greg的观点,两阶段方法是一个合理的解决方案,但我会反过来做。我会这样做:

POST http://server/data/media
body:
{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873
}

创建元数据条目并返回如下响应:

201 Created
Location: http://server/data/media/21323
{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873,
    "ContentUrl": "http://server/data/media/21323/content"
}

然后客户端可以使用这个ContentUrl并对文件数据执行PUT操作。

这种方法的好处是,当您的服务器开始被巨大的数据量压垮时,您返回的url可以指向其他具有更大空间/容量的服务器。或者,如果带宽是个问题,您可以实现某种轮询方法。

我不明白为什么在过去的八年中,没有人给出一个简单的答案。与其将文件编码为base64,不如将json编码为字符串。然后在服务器端解码json。

在Javascript中:

let formData = new FormData();
formData.append("file", myfile);
formData.append("myjson", JSON.stringify(myJsonObject));

POST使用Content-Type: multipart/form-data

在服务器端,正常检索文件,并以字符串的形式检索json。将字符串转换为对象,无论使用何种编程语言,通常都是一行代码。

(是的,效果很好。在我的一个应用程序中。)

我知道这是一个非常古老的问题,但希望这能帮助其他人,因为我看到这篇文章也是为了寻找同样的东西。我有一个类似的问题,只是我的元数据是一个Guid和int。解决方法是一样的。您可以将所需的元数据作为URL的一部分。

在你的“Controller”类中的POST接受方法:

public Task<HttpResponseMessage> PostFile(string name, float latitude, float longitude)
{
    //See http://stackoverflow.com/a/10327789/431906 for how to accept a file
    return null;
}

然后在你注册路由的地方,WebApiConfig。在这种情况下为我注册(HttpConfiguration配置)。

config.Routes.MapHttpRoute(
    name: "FooController",
    routeTemplate: "api/{controller}/{name}/{latitude}/{longitude}",
    defaults: new { }
);