Mercurial > hg > orthanc
comparison OrthancServer/OrthancRestApi/OrthancRestResources.cpp @ 751:5197fd35333c
refactoring of OrthancRestApi
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 14 Apr 2014 11:28:35 +0200 |
parents | |
children | a60040857ce6 |
comparison
equal
deleted
inserted
replaced
750:4afad8cb94fd | 751:5197fd35333c |
---|---|
1 /** | |
2 * Orthanc - A Lightweight, RESTful DICOM Store | |
3 * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, | |
4 * Belgium | |
5 * | |
6 * This program is free software: you can redistribute it and/or | |
7 * modify it under the terms of the GNU General Public License as | |
8 * published by the Free Software Foundation, either version 3 of the | |
9 * License, or (at your option) any later version. | |
10 * | |
11 * In addition, as a special exception, the copyright holders of this | |
12 * program give permission to link the code of its release with the | |
13 * OpenSSL project's "OpenSSL" library (or with modified versions of it | |
14 * that use the same license as the "OpenSSL" library), and distribute | |
15 * the linked executables. You must obey the GNU General Public License | |
16 * in all respects for all of the code used other than "OpenSSL". If you | |
17 * modify file(s) with this exception, you may extend this exception to | |
18 * your version of the file(s), but you are not obligated to do so. If | |
19 * you do not wish to do so, delete this exception statement from your | |
20 * version. If you delete this exception statement from all source files | |
21 * in the program, then also delete it here. | |
22 * | |
23 * This program is distributed in the hope that it will be useful, but | |
24 * WITHOUT ANY WARRANTY; without even the implied warranty of | |
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
26 * General Public License for more details. | |
27 * | |
28 * You should have received a copy of the GNU General Public License | |
29 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
30 **/ | |
31 | |
32 | |
33 #include "OrthancRestApi.h" | |
34 | |
35 #include "../ServerToolbox.h" | |
36 | |
37 #include <glog/logging.h> | |
38 | |
39 namespace Orthanc | |
40 { | |
41 // List all the patients, studies, series or instances ---------------------- | |
42 | |
43 template <enum ResourceType resourceType> | |
44 static void ListResources(RestApi::GetCall& call) | |
45 { | |
46 Json::Value result; | |
47 OrthancRestApi::GetIndex(call).GetAllUuids(result, resourceType); | |
48 call.GetOutput().AnswerJson(result); | |
49 } | |
50 | |
51 template <enum ResourceType resourceType> | |
52 static void GetSingleResource(RestApi::GetCall& call) | |
53 { | |
54 Json::Value result; | |
55 if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType)) | |
56 { | |
57 call.GetOutput().AnswerJson(result); | |
58 } | |
59 } | |
60 | |
61 template <enum ResourceType resourceType> | |
62 static void DeleteSingleResource(RestApi::DeleteCall& call) | |
63 { | |
64 Json::Value result; | |
65 if (OrthancRestApi::GetIndex(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType)) | |
66 { | |
67 call.GetOutput().AnswerJson(result); | |
68 } | |
69 } | |
70 | |
71 | |
72 // Get information about a single patient ----------------------------------- | |
73 | |
74 static void IsProtectedPatient(RestApi::GetCall& call) | |
75 { | |
76 std::string publicId = call.GetUriComponent("id", ""); | |
77 bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId); | |
78 call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", "text/plain"); | |
79 } | |
80 | |
81 | |
82 static void SetPatientProtection(RestApi::PutCall& call) | |
83 { | |
84 ServerContext& context = OrthancRestApi::GetContext(call); | |
85 | |
86 std::string publicId = call.GetUriComponent("id", ""); | |
87 std::string s = Toolbox::StripSpaces(call.GetPutBody()); | |
88 | |
89 if (s == "0") | |
90 { | |
91 context.GetIndex().SetProtectedPatient(publicId, false); | |
92 call.GetOutput().AnswerBuffer("", "text/plain"); | |
93 } | |
94 else if (s == "1") | |
95 { | |
96 context.GetIndex().SetProtectedPatient(publicId, true); | |
97 call.GetOutput().AnswerBuffer("", "text/plain"); | |
98 } | |
99 else | |
100 { | |
101 // Bad request | |
102 } | |
103 } | |
104 | |
105 | |
106 // Get information about a single instance ---------------------------------- | |
107 | |
108 static void GetInstanceFile(RestApi::GetCall& call) | |
109 { | |
110 ServerContext& context = OrthancRestApi::GetContext(call); | |
111 | |
112 std::string publicId = call.GetUriComponent("id", ""); | |
113 context.AnswerDicomFile(call.GetOutput(), publicId, FileContentType_Dicom); | |
114 } | |
115 | |
116 | |
117 static void ExportInstanceFile(RestApi::PostCall& call) | |
118 { | |
119 ServerContext& context = OrthancRestApi::GetContext(call); | |
120 | |
121 std::string publicId = call.GetUriComponent("id", ""); | |
122 | |
123 std::string dicom; | |
124 context.ReadFile(dicom, publicId, FileContentType_Dicom); | |
125 | |
126 Toolbox::WriteFile(dicom, call.GetPostBody()); | |
127 | |
128 call.GetOutput().AnswerBuffer("{}", "application/json"); | |
129 } | |
130 | |
131 | |
132 template <bool simplify> | |
133 static void GetInstanceTags(RestApi::GetCall& call) | |
134 { | |
135 ServerContext& context = OrthancRestApi::GetContext(call); | |
136 | |
137 std::string publicId = call.GetUriComponent("id", ""); | |
138 | |
139 Json::Value full; | |
140 context.ReadJson(full, publicId); | |
141 | |
142 if (simplify) | |
143 { | |
144 Json::Value simplified; | |
145 SimplifyTags(simplified, full); | |
146 call.GetOutput().AnswerJson(simplified); | |
147 } | |
148 else | |
149 { | |
150 call.GetOutput().AnswerJson(full); | |
151 } | |
152 } | |
153 | |
154 | |
155 static void ListFrames(RestApi::GetCall& call) | |
156 { | |
157 Json::Value instance; | |
158 if (OrthancRestApi::GetIndex(call).LookupResource(instance, call.GetUriComponent("id", ""), ResourceType_Instance)) | |
159 { | |
160 unsigned int numberOfFrames = 1; | |
161 | |
162 try | |
163 { | |
164 Json::Value tmp = instance["MainDicomTags"]["NumberOfFrames"]; | |
165 numberOfFrames = boost::lexical_cast<unsigned int>(tmp.asString()); | |
166 } | |
167 catch (...) | |
168 { | |
169 } | |
170 | |
171 Json::Value result = Json::arrayValue; | |
172 for (unsigned int i = 0; i < numberOfFrames; i++) | |
173 { | |
174 result.append(i); | |
175 } | |
176 | |
177 call.GetOutput().AnswerJson(result); | |
178 } | |
179 } | |
180 | |
181 | |
182 template <enum ImageExtractionMode mode> | |
183 static void GetImage(RestApi::GetCall& call) | |
184 { | |
185 ServerContext& context = OrthancRestApi::GetContext(call); | |
186 | |
187 std::string frameId = call.GetUriComponent("frame", "0"); | |
188 | |
189 unsigned int frame; | |
190 try | |
191 { | |
192 frame = boost::lexical_cast<unsigned int>(frameId); | |
193 } | |
194 catch (boost::bad_lexical_cast) | |
195 { | |
196 return; | |
197 } | |
198 | |
199 std::string publicId = call.GetUriComponent("id", ""); | |
200 std::string dicomContent, png; | |
201 context.ReadFile(dicomContent, publicId, FileContentType_Dicom); | |
202 | |
203 try | |
204 { | |
205 FromDcmtkBridge::ExtractPngImage(png, dicomContent, frame, mode); | |
206 call.GetOutput().AnswerBuffer(png, "image/png"); | |
207 } | |
208 catch (OrthancException& e) | |
209 { | |
210 if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange) | |
211 { | |
212 // The frame number is out of the range for this DICOM | |
213 // instance, the resource is not existent | |
214 } | |
215 else | |
216 { | |
217 std::string root = ""; | |
218 for (size_t i = 1; i < call.GetFullUri().size(); i++) | |
219 { | |
220 root += "../"; | |
221 } | |
222 | |
223 call.GetOutput().Redirect(root + "app/images/unsupported.png"); | |
224 } | |
225 } | |
226 } | |
227 | |
228 | |
229 | |
230 static void GetResourceStatistics(RestApi::GetCall& call) | |
231 { | |
232 std::string publicId = call.GetUriComponent("id", ""); | |
233 Json::Value result; | |
234 OrthancRestApi::GetIndex(call).GetStatistics(result, publicId); | |
235 call.GetOutput().AnswerJson(result); | |
236 } | |
237 | |
238 | |
239 | |
240 // Handling of metadata ----------------------------------------------------- | |
241 | |
242 static void CheckValidResourceType(RestApi::Call& call) | |
243 { | |
244 std::string resourceType = call.GetUriComponent("resourceType", ""); | |
245 StringToResourceType(resourceType.c_str()); | |
246 } | |
247 | |
248 | |
249 static void ListMetadata(RestApi::GetCall& call) | |
250 { | |
251 CheckValidResourceType(call); | |
252 | |
253 std::string publicId = call.GetUriComponent("id", ""); | |
254 std::list<MetadataType> metadata; | |
255 | |
256 OrthancRestApi::GetIndex(call).ListAvailableMetadata(metadata, publicId); | |
257 Json::Value result = Json::arrayValue; | |
258 | |
259 for (std::list<MetadataType>::const_iterator | |
260 it = metadata.begin(); it != metadata.end(); ++it) | |
261 { | |
262 result.append(EnumerationToString(*it)); | |
263 } | |
264 | |
265 call.GetOutput().AnswerJson(result); | |
266 } | |
267 | |
268 | |
269 static void GetMetadata(RestApi::GetCall& call) | |
270 { | |
271 CheckValidResourceType(call); | |
272 | |
273 std::string publicId = call.GetUriComponent("id", ""); | |
274 std::string name = call.GetUriComponent("name", ""); | |
275 MetadataType metadata = StringToMetadata(name); | |
276 | |
277 std::string value; | |
278 if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, metadata)) | |
279 { | |
280 call.GetOutput().AnswerBuffer(value, "text/plain"); | |
281 } | |
282 } | |
283 | |
284 | |
285 static void DeleteMetadata(RestApi::DeleteCall& call) | |
286 { | |
287 CheckValidResourceType(call); | |
288 | |
289 std::string publicId = call.GetUriComponent("id", ""); | |
290 std::string name = call.GetUriComponent("name", ""); | |
291 MetadataType metadata = StringToMetadata(name); | |
292 | |
293 if (metadata >= MetadataType_StartUser && | |
294 metadata <= MetadataType_EndUser) | |
295 { | |
296 // It is forbidden to modify internal metadata | |
297 OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata); | |
298 call.GetOutput().AnswerBuffer("", "text/plain"); | |
299 } | |
300 } | |
301 | |
302 | |
303 static void SetMetadata(RestApi::PutCall& call) | |
304 { | |
305 CheckValidResourceType(call); | |
306 | |
307 std::string publicId = call.GetUriComponent("id", ""); | |
308 std::string name = call.GetUriComponent("name", ""); | |
309 MetadataType metadata = StringToMetadata(name); | |
310 std::string value = call.GetPutBody(); | |
311 | |
312 if (metadata >= MetadataType_StartUser && | |
313 metadata <= MetadataType_EndUser) | |
314 { | |
315 // It is forbidden to modify internal metadata | |
316 OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value); | |
317 call.GetOutput().AnswerBuffer("", "text/plain"); | |
318 } | |
319 } | |
320 | |
321 | |
322 | |
323 | |
324 // Handling of attached files ----------------------------------------------- | |
325 | |
326 static void ListAttachments(RestApi::GetCall& call) | |
327 { | |
328 std::string resourceType = call.GetUriComponent("resourceType", ""); | |
329 std::string publicId = call.GetUriComponent("id", ""); | |
330 std::list<FileContentType> attachments; | |
331 OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str())); | |
332 | |
333 Json::Value result = Json::arrayValue; | |
334 | |
335 for (std::list<FileContentType>::const_iterator | |
336 it = attachments.begin(); it != attachments.end(); ++it) | |
337 { | |
338 result.append(EnumerationToString(*it)); | |
339 } | |
340 | |
341 call.GetOutput().AnswerJson(result); | |
342 } | |
343 | |
344 | |
345 static bool GetAttachmentInfo(FileInfo& info, RestApi::Call& call) | |
346 { | |
347 CheckValidResourceType(call); | |
348 | |
349 std::string publicId = call.GetUriComponent("id", ""); | |
350 std::string name = call.GetUriComponent("name", ""); | |
351 FileContentType contentType = StringToContentType(name); | |
352 | |
353 return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType); | |
354 } | |
355 | |
356 | |
357 static void GetAttachmentOperations(RestApi::GetCall& call) | |
358 { | |
359 FileInfo info; | |
360 if (GetAttachmentInfo(info, call)) | |
361 { | |
362 Json::Value operations = Json::arrayValue; | |
363 | |
364 operations.append("compressed-data"); | |
365 | |
366 if (info.GetCompressedMD5() != "") | |
367 { | |
368 operations.append("compressed-md5"); | |
369 } | |
370 | |
371 operations.append("compressed-size"); | |
372 operations.append("data"); | |
373 | |
374 if (info.GetUncompressedMD5() != "") | |
375 { | |
376 operations.append("md5"); | |
377 } | |
378 | |
379 operations.append("size"); | |
380 | |
381 if (info.GetCompressedMD5() != "" && | |
382 info.GetUncompressedMD5() != "") | |
383 { | |
384 operations.append("verify-md5"); | |
385 } | |
386 | |
387 call.GetOutput().AnswerJson(operations); | |
388 } | |
389 } | |
390 | |
391 | |
392 template <int uncompress> | |
393 static void GetAttachmentData(RestApi::GetCall& call) | |
394 { | |
395 ServerContext& context = OrthancRestApi::GetContext(call); | |
396 | |
397 CheckValidResourceType(call); | |
398 | |
399 std::string publicId = call.GetUriComponent("id", ""); | |
400 std::string name = call.GetUriComponent("name", ""); | |
401 | |
402 std::string content; | |
403 context.ReadFile(content, publicId, StringToContentType(name), | |
404 (uncompress == 1)); | |
405 | |
406 call.GetOutput().AnswerBuffer(content, "application/octet-stream"); | |
407 } | |
408 | |
409 | |
410 static void GetAttachmentSize(RestApi::GetCall& call) | |
411 { | |
412 FileInfo info; | |
413 if (GetAttachmentInfo(info, call)) | |
414 { | |
415 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedSize()), "text/plain"); | |
416 } | |
417 } | |
418 | |
419 | |
420 static void GetAttachmentCompressedSize(RestApi::GetCall& call) | |
421 { | |
422 FileInfo info; | |
423 if (GetAttachmentInfo(info, call)) | |
424 { | |
425 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedSize()), "text/plain"); | |
426 } | |
427 } | |
428 | |
429 | |
430 static void GetAttachmentMD5(RestApi::GetCall& call) | |
431 { | |
432 FileInfo info; | |
433 if (GetAttachmentInfo(info, call) && | |
434 info.GetUncompressedMD5() != "") | |
435 { | |
436 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedMD5()), "text/plain"); | |
437 } | |
438 } | |
439 | |
440 | |
441 static void GetAttachmentCompressedMD5(RestApi::GetCall& call) | |
442 { | |
443 FileInfo info; | |
444 if (GetAttachmentInfo(info, call) && | |
445 info.GetCompressedMD5() != "") | |
446 { | |
447 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedMD5()), "text/plain"); | |
448 } | |
449 } | |
450 | |
451 | |
452 static void VerifyAttachment(RestApi::PostCall& call) | |
453 { | |
454 ServerContext& context = OrthancRestApi::GetContext(call); | |
455 CheckValidResourceType(call); | |
456 | |
457 std::string publicId = call.GetUriComponent("id", ""); | |
458 std::string name = call.GetUriComponent("name", ""); | |
459 | |
460 FileInfo info; | |
461 if (!GetAttachmentInfo(info, call) || | |
462 info.GetCompressedMD5() == "" || | |
463 info.GetUncompressedMD5() == "") | |
464 { | |
465 // Inexistent resource, or no MD5 available | |
466 return; | |
467 } | |
468 | |
469 bool ok = false; | |
470 | |
471 // First check whether the compressed data is correctly stored in the disk | |
472 std::string data; | |
473 context.ReadFile(data, publicId, StringToContentType(name), false); | |
474 | |
475 std::string actualMD5; | |
476 Toolbox::ComputeMD5(actualMD5, data); | |
477 | |
478 if (actualMD5 == info.GetCompressedMD5()) | |
479 { | |
480 // The compressed data is OK. If a compression algorithm was | |
481 // applied to it, now check the MD5 of the uncompressed data. | |
482 if (info.GetCompressionType() == CompressionType_None) | |
483 { | |
484 ok = true; | |
485 } | |
486 else | |
487 { | |
488 context.ReadFile(data, publicId, StringToContentType(name), true); | |
489 Toolbox::ComputeMD5(actualMD5, data); | |
490 ok = (actualMD5 == info.GetUncompressedMD5()); | |
491 } | |
492 } | |
493 | |
494 if (ok) | |
495 { | |
496 LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has the right MD5"; | |
497 call.GetOutput().AnswerBuffer("{}", "application/json"); | |
498 } | |
499 else | |
500 { | |
501 LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has bad MD5!"; | |
502 } | |
503 } | |
504 | |
505 | |
506 static void UploadAttachment(RestApi::PutCall& call) | |
507 { | |
508 ServerContext& context = OrthancRestApi::GetContext(call); | |
509 CheckValidResourceType(call); | |
510 | |
511 std::string publicId = call.GetUriComponent("id", ""); | |
512 std::string name = call.GetUriComponent("name", ""); | |
513 | |
514 const void* data = call.GetPutBody().size() ? &call.GetPutBody()[0] : NULL; | |
515 | |
516 FileContentType contentType = StringToContentType(name); | |
517 if (contentType >= FileContentType_StartUser && // It is forbidden to modify internal attachments | |
518 contentType <= FileContentType_EndUser && | |
519 context.AddAttachment(publicId, StringToContentType(name), data, call.GetPutBody().size())) | |
520 { | |
521 call.GetOutput().AnswerBuffer("{}", "application/json"); | |
522 } | |
523 } | |
524 | |
525 | |
526 static void DeleteAttachment(RestApi::DeleteCall& call) | |
527 { | |
528 CheckValidResourceType(call); | |
529 | |
530 std::string publicId = call.GetUriComponent("id", ""); | |
531 std::string name = call.GetUriComponent("name", ""); | |
532 FileContentType contentType = StringToContentType(name); | |
533 | |
534 if (contentType >= FileContentType_StartUser && | |
535 contentType <= FileContentType_EndUser) | |
536 { | |
537 // It is forbidden to delete internal attachments | |
538 OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType); | |
539 call.GetOutput().AnswerBuffer("{}", "application/json"); | |
540 } | |
541 } | |
542 | |
543 | |
544 | |
545 | |
546 void OrthancRestApi::RegisterResources() | |
547 { | |
548 Register("/instances", ListResources<ResourceType_Instance>); | |
549 Register("/patients", ListResources<ResourceType_Patient>); | |
550 Register("/series", ListResources<ResourceType_Series>); | |
551 Register("/studies", ListResources<ResourceType_Study>); | |
552 | |
553 Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>); | |
554 Register("/instances/{id}", GetSingleResource<ResourceType_Instance>); | |
555 Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>); | |
556 Register("/patients/{id}", GetSingleResource<ResourceType_Patient>); | |
557 Register("/series/{id}", DeleteSingleResource<ResourceType_Series>); | |
558 Register("/series/{id}", GetSingleResource<ResourceType_Series>); | |
559 Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>); | |
560 Register("/studies/{id}", GetSingleResource<ResourceType_Study>); | |
561 | |
562 Register("/instances/{id}/statistics", GetResourceStatistics); | |
563 Register("/patients/{id}/statistics", GetResourceStatistics); | |
564 Register("/studies/{id}/statistics", GetResourceStatistics); | |
565 Register("/series/{id}/statistics", GetResourceStatistics); | |
566 | |
567 Register("/instances/{id}/file", GetInstanceFile); | |
568 Register("/instances/{id}/export", ExportInstanceFile); | |
569 Register("/instances/{id}/tags", GetInstanceTags<false>); | |
570 Register("/instances/{id}/simplified-tags", GetInstanceTags<true>); | |
571 Register("/instances/{id}/frames", ListFrames); | |
572 | |
573 Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>); | |
574 Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>); | |
575 Register("/instances/{id}/frames/{frame}/image-uint16", GetImage<ImageExtractionMode_UInt16>); | |
576 Register("/instances/{id}/frames/{frame}/image-int16", GetImage<ImageExtractionMode_Int16>); | |
577 Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>); | |
578 Register("/instances/{id}/image-uint8", GetImage<ImageExtractionMode_UInt8>); | |
579 Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>); | |
580 Register("/instances/{id}/image-int16", GetImage<ImageExtractionMode_Int16>); | |
581 | |
582 Register("/patients/{id}/protected", IsProtectedPatient); | |
583 Register("/patients/{id}/protected", SetPatientProtection); | |
584 | |
585 Register("/{resourceType}/{id}/metadata", ListMetadata); | |
586 Register("/{resourceType}/{id}/metadata/{name}", DeleteMetadata); | |
587 Register("/{resourceType}/{id}/metadata/{name}", GetMetadata); | |
588 Register("/{resourceType}/{id}/metadata/{name}", SetMetadata); | |
589 | |
590 Register("/{resourceType}/{id}/attachments", ListAttachments); | |
591 Register("/{resourceType}/{id}/attachments/{name}", DeleteAttachment); | |
592 Register("/{resourceType}/{id}/attachments/{name}", GetAttachmentOperations); | |
593 Register("/{resourceType}/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>); | |
594 Register("/{resourceType}/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5); | |
595 Register("/{resourceType}/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize); | |
596 Register("/{resourceType}/{id}/attachments/{name}/data", GetAttachmentData<1>); | |
597 Register("/{resourceType}/{id}/attachments/{name}/md5", GetAttachmentMD5); | |
598 Register("/{resourceType}/{id}/attachments/{name}/size", GetAttachmentSize); | |
599 Register("/{resourceType}/{id}/attachments/{name}/verify-md5", VerifyAttachment); | |
600 Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment); | |
601 } | |
602 } |