VirtualBox

source: vbox/trunk/src/VBox/Frontends/VBoxManage/VBoxManageDisk.cpp@ 35146

最後變更 在這個檔案從35146是 35085,由 vboxsync 提交於 14 年 前

VBoxManage: add support for readonly/multiattach media types

  • 屬性 svn:eol-style 設為 native
  • 屬性 svn:keywords 設為 Author Date Id Revision
檔案大小: 42.8 KB
 
1/* $Id: VBoxManageDisk.cpp 35085 2010-12-14 14:09:12Z vboxsync $ */
2/** @file
3 * VBoxManage - The disk related commands.
4 */
5
6/*
7 * Copyright (C) 2006-2010 Oracle Corporation
8 *
9 * This file is part of VirtualBox Open Source Edition (OSE), as
10 * available from http://www.alldomusa.eu.org. This file is free software;
11 * you can redistribute it and/or modify it under the terms of the GNU
12 * General Public License (GPL) as published by the Free Software
13 * Foundation, in version 2 as it comes in the "COPYING" file of the
14 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
15 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
16 */
17
18#ifndef VBOX_ONLY_DOCS
19
20/*******************************************************************************
21* Header Files *
22*******************************************************************************/
23#include <VBox/com/com.h>
24#include <VBox/com/array.h>
25#include <VBox/com/ErrorInfo.h>
26#include <VBox/com/errorprint.h>
27#include <VBox/com/VirtualBox.h>
28
29#include <iprt/asm.h>
30#include <iprt/file.h>
31#include <iprt/path.h>
32#include <iprt/param.h>
33#include <iprt/stream.h>
34#include <iprt/string.h>
35#include <iprt/ctype.h>
36#include <iprt/getopt.h>
37#include <VBox/log.h>
38#include <VBox/vd.h>
39
40#include "VBoxManage.h"
41using namespace com;
42
43
44// funcs
45///////////////////////////////////////////////////////////////////////////////
46
47
48static DECLCALLBACK(void) handleVDError(void *pvUser, int rc, RT_SRC_POS_DECL, const char *pszFormat, va_list va)
49{
50 RTMsgError(pszFormat, va);
51 RTMsgError("Error code %Rrc at %s(%u) in function %s", rc, RT_SRC_POS_ARGS);
52}
53
54
55static int parseDiskVariant(const char *psz, MediumVariant_T *pDiskVariant)
56{
57 int rc = VINF_SUCCESS;
58 unsigned DiskVariant = (unsigned)(*pDiskVariant);
59 while (psz && *psz && RT_SUCCESS(rc))
60 {
61 size_t len;
62 const char *pszComma = strchr(psz, ',');
63 if (pszComma)
64 len = pszComma - psz;
65 else
66 len = strlen(psz);
67 if (len > 0)
68 {
69 // Parsing is intentionally inconsistent: "standard" resets the
70 // variant, whereas the other flags are cumulative.
71 if (!RTStrNICmp(psz, "standard", len))
72 DiskVariant = MediumVariant_Standard;
73 else if ( !RTStrNICmp(psz, "fixed", len)
74 || !RTStrNICmp(psz, "static", len))
75 DiskVariant |= MediumVariant_Fixed;
76 else if (!RTStrNICmp(psz, "Diff", len))
77 DiskVariant |= MediumVariant_Diff;
78 else if (!RTStrNICmp(psz, "split2g", len))
79 DiskVariant |= MediumVariant_VmdkSplit2G;
80 else if ( !RTStrNICmp(psz, "stream", len)
81 || !RTStrNICmp(psz, "streamoptimized", len))
82 DiskVariant |= MediumVariant_VmdkStreamOptimized;
83 else if (!RTStrNICmp(psz, "esx", len))
84 DiskVariant |= MediumVariant_VmdkESX;
85 else
86 rc = VERR_PARSE_ERROR;
87 }
88 if (pszComma)
89 psz += len + 1;
90 else
91 psz += len;
92 }
93
94 if (RT_SUCCESS(rc))
95 *pDiskVariant = (MediumVariant_T)DiskVariant;
96 return rc;
97}
98
99int parseDiskType(const char *psz, MediumType_T *pDiskType)
100{
101 int rc = VINF_SUCCESS;
102 MediumType_T DiskType = MediumType_Normal;
103 if (!RTStrICmp(psz, "normal"))
104 DiskType = MediumType_Normal;
105 else if (!RTStrICmp(psz, "immutable"))
106 DiskType = MediumType_Immutable;
107 else if (!RTStrICmp(psz, "writethrough"))
108 DiskType = MediumType_Writethrough;
109 else if (!RTStrICmp(psz, "shareable"))
110 DiskType = MediumType_Shareable;
111 else if (!RTStrICmp(psz, "readonly"))
112 DiskType = MediumType_Readonly;
113 else if (!RTStrICmp(psz, "multiattach"))
114 DiskType = MediumType_MultiAttach;
115 else
116 rc = VERR_PARSE_ERROR;
117
118 if (RT_SUCCESS(rc))
119 *pDiskType = DiskType;
120 return rc;
121}
122
123/** @todo move this into getopt, as getting bool values is generic */
124static int parseBool(const char *psz, bool *pb)
125{
126 int rc = VINF_SUCCESS;
127 if ( !RTStrICmp(psz, "on")
128 || !RTStrICmp(psz, "yes")
129 || !RTStrICmp(psz, "true")
130 || !RTStrICmp(psz, "1")
131 || !RTStrICmp(psz, "enable")
132 || !RTStrICmp(psz, "enabled"))
133 {
134 *pb = true;
135 }
136 else if ( !RTStrICmp(psz, "off")
137 || !RTStrICmp(psz, "no")
138 || !RTStrICmp(psz, "false")
139 || !RTStrICmp(psz, "0")
140 || !RTStrICmp(psz, "disable")
141 || !RTStrICmp(psz, "disabled"))
142 {
143 *pb = false;
144 }
145 else
146 rc = VERR_PARSE_ERROR;
147
148 return rc;
149}
150
151static const RTGETOPTDEF g_aCreateHardDiskOptions[] =
152{
153 { "--filename", 'f', RTGETOPT_REQ_STRING },
154 { "-filename", 'f', RTGETOPT_REQ_STRING }, // deprecated
155 { "--size", 's', RTGETOPT_REQ_UINT64 },
156 { "-size", 's', RTGETOPT_REQ_UINT64 }, // deprecated
157 { "--sizebyte", 'S', RTGETOPT_REQ_UINT64 },
158 { "--format", 'o', RTGETOPT_REQ_STRING },
159 { "-format", 'o', RTGETOPT_REQ_STRING }, // deprecated
160 { "--static", 'F', RTGETOPT_REQ_NOTHING },
161 { "-static", 'F', RTGETOPT_REQ_NOTHING }, // deprecated
162 { "--variant", 'm', RTGETOPT_REQ_STRING },
163 { "-variant", 'm', RTGETOPT_REQ_STRING }, // deprecated
164};
165
166int handleCreateHardDisk(HandlerArg *a)
167{
168 HRESULT rc;
169 int vrc;
170 Bstr filename;
171 uint64_t size = 0;
172 Bstr format = "VDI";
173 MediumVariant_T DiskVariant = MediumVariant_Standard;
174
175 int c;
176 RTGETOPTUNION ValueUnion;
177 RTGETOPTSTATE GetState;
178 // start at 0 because main() has hacked both the argc and argv given to us
179 RTGetOptInit(&GetState, a->argc, a->argv, g_aCreateHardDiskOptions, RT_ELEMENTS(g_aCreateHardDiskOptions),
180 0, RTGETOPTINIT_FLAGS_NO_STD_OPTS);
181 while ((c = RTGetOpt(&GetState, &ValueUnion)))
182 {
183 switch (c)
184 {
185 case 'f': // --filename
186 filename = ValueUnion.psz;
187 break;
188
189 case 's': // --size
190 size = ValueUnion.u64 * _1M;
191 break;
192
193 case 'S': // --sizebyte
194 size = ValueUnion.u64;
195 break;
196
197 case 'o': // --format
198 format = ValueUnion.psz;
199 break;
200
201 case 'F': // --static ("fixed"/"flat")
202 {
203 unsigned uDiskVariant = (unsigned)DiskVariant;
204 uDiskVariant |= MediumVariant_Fixed;
205 DiskVariant = (MediumVariant_T)uDiskVariant;
206 break;
207 }
208
209 case 'm': // --variant
210 vrc = parseDiskVariant(ValueUnion.psz, &DiskVariant);
211 if (RT_FAILURE(vrc))
212 return errorArgument("Invalid hard disk variant '%s'", ValueUnion.psz);
213 break;
214
215 case VINF_GETOPT_NOT_OPTION:
216 return errorSyntax(USAGE_CREATEHD, "Invalid parameter '%s'", ValueUnion.psz);
217
218 default:
219 if (c > 0)
220 {
221 if (RT_C_IS_PRINT(c))
222 return errorSyntax(USAGE_CREATEHD, "Invalid option -%c", c);
223 else
224 return errorSyntax(USAGE_CREATEHD, "Invalid option case %i", c);
225 }
226 else if (c == VERR_GETOPT_UNKNOWN_OPTION)
227 return errorSyntax(USAGE_CREATEHD, "unknown option: %s\n", ValueUnion.psz);
228 else if (ValueUnion.pDef)
229 return errorSyntax(USAGE_CREATEHD, "%s: %Rrs", ValueUnion.pDef->pszLong, c);
230 else
231 return errorSyntax(USAGE_CREATEHD, "error: %Rrs", c);
232 }
233 }
234
235 /* check the outcome */
236 if ( filename.isEmpty()
237 || size == 0)
238 return errorSyntax(USAGE_CREATEHD, "Parameters --filename and --size are required");
239
240 /* check for filename extension */
241 Utf8Str strName(filename);
242 if (!RTPathHaveExt(strName.c_str()))
243 {
244 Utf8Str strFormat(format);
245 if (strFormat.compare("vmdk", iprt::MiniString::CaseInsensitive) == 0)
246 strName.append(".vmdk");
247 else if (strFormat.compare("vhd", iprt::MiniString::CaseInsensitive) == 0)
248 strName.append(".vhd");
249 else
250 strName.append(".vdi");
251 filename = Bstr(strName);
252 }
253
254 ComPtr<IMedium> hardDisk;
255 CHECK_ERROR(a->virtualBox, CreateHardDisk(format.raw(), filename.raw(),
256 hardDisk.asOutParam()));
257 if (SUCCEEDED(rc) && hardDisk)
258 {
259 ComPtr<IProgress> progress;
260 CHECK_ERROR(hardDisk, CreateBaseStorage(size, DiskVariant, progress.asOutParam()));
261 if (SUCCEEDED(rc) && progress)
262 {
263 rc = showProgress(progress);
264 if (FAILED(rc))
265 {
266 com::ProgressErrorInfo info(progress);
267 if (info.isBasicAvailable())
268 RTMsgError("Failed to create hard disk. Error message: %lS", info.getText().raw());
269 else
270 RTMsgError("Failed to create hard disk. No error message available!");
271 }
272 else
273 {
274 Bstr uuid;
275 CHECK_ERROR(hardDisk, COMGETTER(Id)(uuid.asOutParam()));
276 RTPrintf("Disk image created. UUID: %s\n", Utf8Str(uuid).c_str());
277 }
278 }
279 CHECK_ERROR(hardDisk, Close());
280 }
281 return SUCCEEDED(rc) ? 0 : 1;
282}
283
284static const RTGETOPTDEF g_aModifyHardDiskOptions[] =
285{
286 { "--type", 't', RTGETOPT_REQ_STRING },
287 { "-type", 't', RTGETOPT_REQ_STRING }, // deprecated
288 { "settype", 't', RTGETOPT_REQ_STRING }, // deprecated
289 { "--autoreset", 'z', RTGETOPT_REQ_STRING },
290 { "-autoreset", 'z', RTGETOPT_REQ_STRING }, // deprecated
291 { "autoreset", 'z', RTGETOPT_REQ_STRING }, // deprecated
292 { "--compact", 'c', RTGETOPT_REQ_NOTHING },
293 { "-compact", 'c', RTGETOPT_REQ_NOTHING }, // deprecated
294 { "compact", 'c', RTGETOPT_REQ_NOTHING }, // deprecated
295 { "--resize", 'r', RTGETOPT_REQ_UINT64 },
296 { "--resizebyte", 'R', RTGETOPT_REQ_UINT64 }
297};
298
299int handleModifyHardDisk(HandlerArg *a)
300{
301 HRESULT rc;
302 int vrc;
303 ComPtr<IMedium> hardDisk;
304 MediumType_T DiskType;
305 bool AutoReset = false;
306 bool fModifyDiskType = false, fModifyAutoReset = false, fModifyCompact = false;
307 bool fModifyResize = false;
308 uint64_t cbResize = 0;
309 const char *FilenameOrUuid = NULL;
310 bool unknown = false;
311
312 int c;
313 RTGETOPTUNION ValueUnion;
314 RTGETOPTSTATE GetState;
315 // start at 0 because main() has hacked both the argc and argv given to us
316 RTGetOptInit(&GetState, a->argc, a->argv, g_aModifyHardDiskOptions, RT_ELEMENTS(g_aModifyHardDiskOptions),
317 0, RTGETOPTINIT_FLAGS_NO_STD_OPTS);
318 while ((c = RTGetOpt(&GetState, &ValueUnion)))
319 {
320 switch (c)
321 {
322 case 't': // --type
323 vrc = parseDiskType(ValueUnion.psz, &DiskType);
324 if (RT_FAILURE(vrc))
325 return errorArgument("Invalid hard disk type '%s'", ValueUnion.psz);
326 fModifyDiskType = true;
327 break;
328
329 case 'z': // --autoreset
330 vrc = parseBool(ValueUnion.psz, &AutoReset);
331 if (RT_FAILURE(vrc))
332 return errorArgument("Invalid autoreset parameter '%s'", ValueUnion.psz);
333 fModifyAutoReset = true;
334 break;
335
336 case 'c': // --compact
337 fModifyCompact = true;
338 break;
339
340 case 'r': // --resize
341 cbResize = ValueUnion.u64 * _1M;
342 fModifyResize = true;
343 break;
344
345 case 'R': // --resizebyte
346 cbResize = ValueUnion.u64;
347 fModifyResize = true;
348 break;
349
350 case VINF_GETOPT_NOT_OPTION:
351 if (!FilenameOrUuid)
352 FilenameOrUuid = ValueUnion.psz;
353 else
354 return errorSyntax(USAGE_CREATEHD, "Invalid parameter '%s'", ValueUnion.psz);
355 break;
356
357 default:
358 if (c > 0)
359 {
360 if (RT_C_IS_PRINT(c))
361 return errorSyntax(USAGE_MODIFYHD, "Invalid option -%c", c);
362 else
363 return errorSyntax(USAGE_MODIFYHD, "Invalid option case %i", c);
364 }
365 else if (c == VERR_GETOPT_UNKNOWN_OPTION)
366 return errorSyntax(USAGE_MODIFYHD, "unknown option: %s\n", ValueUnion.psz);
367 else if (ValueUnion.pDef)
368 return errorSyntax(USAGE_MODIFYHD, "%s: %Rrs", ValueUnion.pDef->pszLong, c);
369 else
370 return errorSyntax(USAGE_MODIFYHD, "error: %Rrs", c);
371 }
372 }
373
374 if (!FilenameOrUuid)
375 return errorSyntax(USAGE_MODIFYHD, "Disk name or UUID required");
376
377 if (!fModifyDiskType && !fModifyAutoReset && !fModifyCompact && !fModifyResize)
378 return errorSyntax(USAGE_MODIFYHD, "No operation specified");
379
380 /* Depending on the operation the medium must be in the registry or
381 * may be opened on demand. */
382 if (fModifyDiskType || fModifyAutoReset)
383 {
384 CHECK_ERROR(a->virtualBox, FindMedium(Bstr(FilenameOrUuid).raw(),
385 DeviceType_HardDisk,
386 hardDisk.asOutParam()));
387 }
388 else
389 {
390 rc = a->virtualBox->FindMedium(Bstr(FilenameOrUuid).raw(),
391 DeviceType_HardDisk,
392 hardDisk.asOutParam());
393 /* the hard disk image might not be registered */
394 if (!hardDisk)
395 {
396 unknown = true;
397 rc = a->virtualBox->OpenMedium(Bstr(FilenameOrUuid).raw(),
398 DeviceType_HardDisk,
399 AccessMode_ReadWrite,
400 hardDisk.asOutParam());
401 if (rc == VBOX_E_FILE_ERROR)
402 {
403 char szFilenameAbs[RTPATH_MAX] = "";
404 int irc = RTPathAbs(FilenameOrUuid, szFilenameAbs, sizeof(szFilenameAbs));
405 if (RT_FAILURE(irc))
406 {
407 RTMsgError("Cannot convert filename \"%s\" to absolute path", FilenameOrUuid);
408 return 1;
409 }
410 CHECK_ERROR(a->virtualBox, OpenMedium(Bstr(szFilenameAbs).raw(),
411 DeviceType_HardDisk,
412 AccessMode_ReadWrite,
413 hardDisk.asOutParam()));
414 }
415 }
416 }
417 if (FAILED(rc))
418 return 1;
419 if (hardDisk.isNull())
420 {
421 RTMsgError("Invalid hard disk reference, avoiding crash");
422 return 1;
423 }
424
425 if (fModifyDiskType)
426 {
427 MediumType_T hddType;
428 CHECK_ERROR(hardDisk, COMGETTER(Type)(&hddType));
429
430 if (hddType != DiskType)
431 CHECK_ERROR(hardDisk, COMSETTER(Type)(DiskType));
432 }
433
434 if (fModifyAutoReset)
435 {
436 CHECK_ERROR(hardDisk, COMSETTER(AutoReset)(AutoReset));
437 }
438
439 if (fModifyCompact)
440 {
441 ComPtr<IProgress> progress;
442 CHECK_ERROR(hardDisk, Compact(progress.asOutParam()));
443 if (SUCCEEDED(rc))
444 rc = showProgress(progress);
445 if (FAILED(rc))
446 {
447 if (rc == E_NOTIMPL)
448 RTMsgError("Compact hard disk operation is not implemented!");
449 else if (rc == VBOX_E_NOT_SUPPORTED)
450 RTMsgError("Compact hard disk operation for this format is not implemented yet!");
451 else
452 com::GluePrintRCMessage(rc);
453 }
454 }
455
456 if (fModifyResize)
457 {
458 ComPtr<IProgress> progress;
459 CHECK_ERROR(hardDisk, Resize(cbResize, progress.asOutParam()));
460 if (SUCCEEDED(rc))
461 rc = showProgress(progress);
462 if (FAILED(rc))
463 {
464 if (rc == E_NOTIMPL)
465 RTMsgError("Resize hard disk operation is not implemented!");
466 else if (rc == VBOX_E_NOT_SUPPORTED)
467 RTMsgError("Resize hard disk operation for this format is not implemented yet!");
468 else
469 com::GluePrintRCMessage(rc);
470 }
471 }
472
473 if (unknown)
474 hardDisk->Close();
475
476 return SUCCEEDED(rc) ? 0 : 1;
477}
478
479static const RTGETOPTDEF g_aCloneHardDiskOptions[] =
480{
481 { "--format", 'o', RTGETOPT_REQ_STRING },
482 { "-format", 'o', RTGETOPT_REQ_STRING },
483 { "--static", 'F', RTGETOPT_REQ_NOTHING },
484 { "-static", 'F', RTGETOPT_REQ_NOTHING },
485 { "--existing", 'E', RTGETOPT_REQ_NOTHING },
486 { "--variant", 'm', RTGETOPT_REQ_STRING },
487 { "-variant", 'm', RTGETOPT_REQ_STRING },
488};
489
490int handleCloneHardDisk(HandlerArg *a)
491{
492 HRESULT rc;
493 int vrc;
494 Bstr src, dst;
495 Bstr format;
496 MediumVariant_T DiskVariant = MediumVariant_Standard;
497 bool fExisting = false;
498
499 int c;
500 RTGETOPTUNION ValueUnion;
501 RTGETOPTSTATE GetState;
502 // start at 0 because main() has hacked both the argc and argv given to us
503 RTGetOptInit(&GetState, a->argc, a->argv, g_aCloneHardDiskOptions, RT_ELEMENTS(g_aCloneHardDiskOptions),
504 0, RTGETOPTINIT_FLAGS_NO_STD_OPTS);
505 while ((c = RTGetOpt(&GetState, &ValueUnion)))
506 {
507 switch (c)
508 {
509 case 'o': // --format
510 format = ValueUnion.psz;
511 break;
512
513 case 'F': // --static
514 {
515 unsigned uDiskVariant = (unsigned)DiskVariant;
516 uDiskVariant |= MediumVariant_Fixed;
517 DiskVariant = (MediumVariant_T)uDiskVariant;
518 break;
519 }
520
521 case 'E': // --existing
522 fExisting = true;
523 break;
524
525 case 'm': // --variant
526 vrc = parseDiskVariant(ValueUnion.psz, &DiskVariant);
527 if (RT_FAILURE(vrc))
528 return errorArgument("Invalid hard disk variant '%s'", ValueUnion.psz);
529 break;
530
531 case VINF_GETOPT_NOT_OPTION:
532 if (src.isEmpty())
533 src = ValueUnion.psz;
534 else if (dst.isEmpty())
535 dst = ValueUnion.psz;
536 else
537 return errorSyntax(USAGE_CLONEHD, "Invalid parameter '%s'", ValueUnion.psz);
538 break;
539
540 default:
541 if (c > 0)
542 {
543 if (RT_C_IS_GRAPH(c))
544 return errorSyntax(USAGE_CLONEHD, "unhandled option: -%c", c);
545 else
546 return errorSyntax(USAGE_CLONEHD, "unhandled option: %i", c);
547 }
548 else if (c == VERR_GETOPT_UNKNOWN_OPTION)
549 return errorSyntax(USAGE_CLONEHD, "unknown option: %s", ValueUnion.psz);
550 else if (ValueUnion.pDef)
551 return errorSyntax(USAGE_CLONEHD, "%s: %Rrs", ValueUnion.pDef->pszLong, c);
552 else
553 return errorSyntax(USAGE_CLONEHD, "error: %Rrs", c);
554 }
555 }
556
557 if (src.isEmpty())
558 return errorSyntax(USAGE_CLONEHD, "Mandatory UUID or input file parameter missing");
559 if (dst.isEmpty())
560 return errorSyntax(USAGE_CLONEHD, "Mandatory output file parameter missing");
561 if (fExisting && (!format.isEmpty() || DiskVariant != MediumType_Normal))
562 return errorSyntax(USAGE_CLONEHD, "Specified options which cannot be used with --existing");
563
564 ComPtr<IMedium> srcDisk;
565 ComPtr<IMedium> dstDisk;
566 bool fSrcUnknown = false;
567 bool fDstUnknown = false;
568
569 rc = a->virtualBox->FindMedium(src.raw(), DeviceType_HardDisk,
570 srcDisk.asOutParam());
571 /* no? well, then it's an unknown image */
572 if (FAILED (rc))
573 {
574 rc = a->virtualBox->OpenMedium(src.raw(), DeviceType_HardDisk,
575 AccessMode_ReadWrite,
576 srcDisk.asOutParam());
577 if (rc == VBOX_E_FILE_ERROR)
578 {
579 char szFilenameAbs[RTPATH_MAX] = "";
580 int irc = RTPathAbs(Utf8Str(src).c_str(), szFilenameAbs, sizeof(szFilenameAbs));
581 if (RT_FAILURE(irc))
582 {
583 RTMsgError("Cannot convert filename \"%s\" to absolute path", Utf8Str(src).c_str());
584 return 1;
585 }
586 CHECK_ERROR(a->virtualBox, OpenMedium(Bstr(szFilenameAbs).raw(),
587 DeviceType_HardDisk,
588 AccessMode_ReadWrite,
589 srcDisk.asOutParam()));
590 }
591 else if (SUCCEEDED(rc))
592 fSrcUnknown = true;
593 else
594 {
595 com::GluePrintRCMessage(rc);
596 return 1;
597 }
598 }
599
600 do
601 {
602 /* open/create destination hard disk */
603 if (fExisting)
604 {
605 rc = a->virtualBox->FindMedium(dst.raw(), DeviceType_HardDisk,
606 dstDisk.asOutParam());
607 /* no? well, then it's an unknown image */
608 if (FAILED(rc))
609 {
610 rc = a->virtualBox->OpenMedium(dst.raw(), DeviceType_HardDisk,
611 AccessMode_ReadWrite,
612 dstDisk.asOutParam());
613 if (rc == VBOX_E_FILE_ERROR)
614 {
615 char szFilenameAbs[RTPATH_MAX] = "";
616 int irc = RTPathAbs(Utf8Str(dst).c_str(), szFilenameAbs, sizeof(szFilenameAbs));
617 if (RT_FAILURE(irc))
618 {
619 RTMsgError("Cannot convert filename \"%s\" to absolute path", Utf8Str(dst).c_str());
620 return 1;
621 }
622 CHECK_ERROR_BREAK(a->virtualBox, OpenMedium(Bstr(szFilenameAbs).raw(),
623 DeviceType_HardDisk,
624 AccessMode_ReadWrite,
625 dstDisk.asOutParam()));
626 }
627 else if (FAILED(rc))
628 {
629 com::GluePrintRCMessage(rc);
630 break;
631 }
632
633 /* If the image wasn't opened before, close it at the end. */
634 if (SUCCEEDED(rc))
635 fDstUnknown = true;
636 }
637 if (SUCCEEDED(rc))
638 {
639 /* Perform accessibility check now. */
640 MediumState_T state;
641 CHECK_ERROR_BREAK(dstDisk, RefreshState(&state));
642 CHECK_ERROR_BREAK(dstDisk, COMGETTER(Format)(format.asOutParam()));
643 }
644 }
645 else
646 {
647 /* use the format of the source hard disk if unspecified */
648 if (format.isEmpty())
649 CHECK_ERROR_BREAK(srcDisk, COMGETTER(Format)(format.asOutParam()));
650 CHECK_ERROR_BREAK(a->virtualBox, CreateHardDisk(format.raw(),
651 dst.raw(),
652 dstDisk.asOutParam()));
653 }
654
655 ComPtr<IProgress> progress;
656 CHECK_ERROR_BREAK(srcDisk, CloneTo(dstDisk, DiskVariant, NULL, progress.asOutParam()));
657
658 rc = showProgress(progress);
659 if (FAILED(rc))
660 {
661 com::ProgressErrorInfo info(progress);
662 if (info.isBasicAvailable())
663 RTMsgError("Failed to clone hard disk. Error message: %lS", info.getText().raw());
664 else
665 RTMsgError("Failed to clone hard disk. No error message available!");
666 break;
667 }
668
669 Bstr uuid;
670 CHECK_ERROR_BREAK(dstDisk, COMGETTER(Id)(uuid.asOutParam()));
671
672 RTPrintf("Clone hard disk created in format '%ls'. UUID: %s\n",
673 format.raw(), Utf8Str(uuid).c_str());
674 }
675 while (0);
676
677 if (fDstUnknown && !dstDisk.isNull())
678 {
679 /* forget the created clone */
680 dstDisk->Close();
681 }
682 if (fSrcUnknown)
683 {
684 /* close the unknown hard disk to forget it again */
685 srcDisk->Close();
686 }
687
688 return SUCCEEDED(rc) ? 0 : 1;
689}
690
691static const RTGETOPTDEF g_aConvertFromRawHardDiskOptions[] =
692{
693 { "--format", 'o', RTGETOPT_REQ_STRING },
694 { "-format", 'o', RTGETOPT_REQ_STRING },
695 { "--static", 'F', RTGETOPT_REQ_NOTHING },
696 { "-static", 'F', RTGETOPT_REQ_NOTHING },
697 { "--variant", 'm', RTGETOPT_REQ_STRING },
698 { "-variant", 'm', RTGETOPT_REQ_STRING },
699};
700
701RTEXITCODE handleConvertFromRaw(int argc, char *argv[])
702{
703 int rc = VINF_SUCCESS;
704 bool fReadFromStdIn = false;
705 const char *format = "VDI";
706 const char *srcfilename = NULL;
707 const char *dstfilename = NULL;
708 const char *filesize = NULL;
709 unsigned uImageFlags = VD_IMAGE_FLAGS_NONE;
710 void *pvBuf = NULL;
711
712 int c;
713 RTGETOPTUNION ValueUnion;
714 RTGETOPTSTATE GetState;
715 // start at 0 because main() has hacked both the argc and argv given to us
716 RTGetOptInit(&GetState, argc, argv, g_aConvertFromRawHardDiskOptions, RT_ELEMENTS(g_aConvertFromRawHardDiskOptions),
717 0, RTGETOPTINIT_FLAGS_NO_STD_OPTS);
718 while ((c = RTGetOpt(&GetState, &ValueUnion)))
719 {
720 switch (c)
721 {
722 case 'o': // --format
723 format = ValueUnion.psz;
724 break;
725
726 case 'm': // --variant
727 MediumVariant_T DiskVariant;
728 rc = parseDiskVariant(ValueUnion.psz, &DiskVariant);
729 if (RT_FAILURE(rc))
730 return errorArgument("Invalid hard disk variant '%s'", ValueUnion.psz);
731 /// @todo cleaner solution than assuming 1:1 mapping?
732 uImageFlags = (unsigned)DiskVariant;
733 break;
734
735 case VINF_GETOPT_NOT_OPTION:
736 if (!srcfilename)
737 {
738 srcfilename = ValueUnion.psz;
739// If you change the OS list here don't forget to update VBoxManageHelp.cpp.
740#ifndef RT_OS_WINDOWS
741 fReadFromStdIn = !strcmp(srcfilename, "stdin");
742#endif
743 }
744 else if (!dstfilename)
745 dstfilename = ValueUnion.psz;
746 else if (fReadFromStdIn && !filesize)
747 filesize = ValueUnion.psz;
748 else
749 return errorSyntax(USAGE_CONVERTFROMRAW, "Invalid parameter '%s'", ValueUnion.psz);
750 break;
751
752 default:
753 return errorGetOpt(USAGE_CONVERTFROMRAW, c, &ValueUnion);
754 }
755 }
756
757 if (!srcfilename || !dstfilename || (fReadFromStdIn && !filesize))
758 return errorSyntax(USAGE_CONVERTFROMRAW, "Incorrect number of parameters");
759 RTStrmPrintf(g_pStdErr, "Converting from raw image file=\"%s\" to file=\"%s\"...\n",
760 srcfilename, dstfilename);
761
762 PVBOXHDD pDisk = NULL;
763
764 PVDINTERFACE pVDIfs = NULL;
765 VDINTERFACE vdInterfaceError;
766 VDINTERFACEERROR vdInterfaceErrorCallbacks;
767 vdInterfaceErrorCallbacks.cbSize = sizeof(VDINTERFACEERROR);
768 vdInterfaceErrorCallbacks.enmInterface = VDINTERFACETYPE_ERROR;
769 vdInterfaceErrorCallbacks.pfnError = handleVDError;
770 vdInterfaceErrorCallbacks.pfnMessage = NULL;
771
772 rc = VDInterfaceAdd(&vdInterfaceError, "VBoxManage_IError", VDINTERFACETYPE_ERROR,
773 &vdInterfaceErrorCallbacks, NULL, &pVDIfs);
774 AssertRC(rc);
775
776 /* open raw image file. */
777 RTFILE File;
778 if (fReadFromStdIn)
779 File = 0;
780 else
781 rc = RTFileOpen(&File, srcfilename, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_WRITE);
782 if (RT_FAILURE(rc))
783 {
784 RTMsgError("Cannot open file \"%s\": %Rrc", srcfilename, rc);
785 goto out;
786 }
787
788 uint64_t cbFile;
789 /* get image size. */
790 if (fReadFromStdIn)
791 cbFile = RTStrToUInt64(filesize);
792 else
793 rc = RTFileGetSize(File, &cbFile);
794 if (RT_FAILURE(rc))
795 {
796 RTMsgError("Cannot get image size for file \"%s\": %Rrc", srcfilename, rc);
797 goto out;
798 }
799
800 RTStrmPrintf(g_pStdErr, "Creating %s image with size %RU64 bytes (%RU64MB)...\n",
801 (uImageFlags & VD_IMAGE_FLAGS_FIXED) ? "fixed" : "dynamic", cbFile, (cbFile + _1M - 1) / _1M);
802 char pszComment[256];
803 RTStrPrintf(pszComment, sizeof(pszComment), "Converted image from %s", srcfilename);
804 rc = VDCreate(pVDIfs, VDTYPE_HDD, &pDisk);
805 if (RT_FAILURE(rc))
806 {
807 RTMsgError("Cannot create the virtual disk container: %Rrc", rc);
808 goto out;
809 }
810
811 Assert(RT_MIN(cbFile / 512 / 16 / 63, 16383) -
812 (unsigned int)RT_MIN(cbFile / 512 / 16 / 63, 16383) == 0);
813 VDGEOMETRY PCHS, LCHS;
814 PCHS.cCylinders = (unsigned int)RT_MIN(cbFile / 512 / 16 / 63, 16383);
815 PCHS.cHeads = 16;
816 PCHS.cSectors = 63;
817 LCHS.cCylinders = 0;
818 LCHS.cHeads = 0;
819 LCHS.cSectors = 0;
820 rc = VDCreateBase(pDisk, format, dstfilename, cbFile,
821 uImageFlags, pszComment, &PCHS, &LCHS, NULL,
822 VD_OPEN_FLAGS_NORMAL, NULL, NULL);
823 if (RT_FAILURE(rc))
824 {
825 RTMsgError("Cannot create the disk image \"%s\": %Rrc", dstfilename, rc);
826 goto out;
827 }
828
829 size_t cbBuffer;
830 cbBuffer = _1M;
831 pvBuf = RTMemAlloc(cbBuffer);
832 if (!pvBuf)
833 {
834 rc = VERR_NO_MEMORY;
835 RTMsgError("Out of memory allocating buffers for image \"%s\": %Rrc", dstfilename, rc);
836 goto out;
837 }
838
839 uint64_t offFile;
840 offFile = 0;
841 while (offFile < cbFile)
842 {
843 size_t cbRead;
844 size_t cbToRead;
845 cbRead = 0;
846 cbToRead = cbFile - offFile >= (uint64_t)cbBuffer ?
847 cbBuffer : (size_t)(cbFile - offFile);
848 rc = RTFileRead(File, pvBuf, cbToRead, &cbRead);
849 if (RT_FAILURE(rc) || !cbRead)
850 break;
851 rc = VDWrite(pDisk, offFile, pvBuf, cbRead);
852 if (RT_FAILURE(rc))
853 {
854 RTMsgError("Failed to write to disk image \"%s\": %Rrc", dstfilename, rc);
855 goto out;
856 }
857 offFile += cbRead;
858 }
859
860out:
861 if (pvBuf)
862 RTMemFree(pvBuf);
863 if (pDisk)
864 VDClose(pDisk, RT_FAILURE(rc));
865 if (File != NIL_RTFILE)
866 RTFileClose(File);
867
868 return RT_SUCCESS(rc) ? RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE;
869}
870
871static const RTGETOPTDEF g_aShowHardDiskInfoOptions[] =
872{
873 { "--dummy", 256, RTGETOPT_REQ_NOTHING }, // placeholder for C++
874};
875
876int handleShowHardDiskInfo(HandlerArg *a)
877{
878 HRESULT rc;
879 const char *FilenameOrUuid = NULL;
880
881 int c;
882 RTGETOPTUNION ValueUnion;
883 RTGETOPTSTATE GetState;
884 // start at 0 because main() has hacked both the argc and argv given to us
885 RTGetOptInit(&GetState, a->argc, a->argv, g_aShowHardDiskInfoOptions, RT_ELEMENTS(g_aShowHardDiskInfoOptions),
886 0, RTGETOPTINIT_FLAGS_NO_STD_OPTS);
887 while ((c = RTGetOpt(&GetState, &ValueUnion)))
888 {
889 switch (c)
890 {
891 case VINF_GETOPT_NOT_OPTION:
892 if (!FilenameOrUuid)
893 FilenameOrUuid = ValueUnion.psz;
894 else
895 return errorSyntax(USAGE_SHOWHDINFO, "Invalid parameter '%s'", ValueUnion.psz);
896 break;
897
898 default:
899 if (c > 0)
900 {
901 if (RT_C_IS_PRINT(c))
902 return errorSyntax(USAGE_SHOWHDINFO, "Invalid option -%c", c);
903 else
904 return errorSyntax(USAGE_SHOWHDINFO, "Invalid option case %i", c);
905 }
906 else if (c == VERR_GETOPT_UNKNOWN_OPTION)
907 return errorSyntax(USAGE_SHOWHDINFO, "unknown option: %s\n", ValueUnion.psz);
908 else if (ValueUnion.pDef)
909 return errorSyntax(USAGE_SHOWHDINFO, "%s: %Rrs", ValueUnion.pDef->pszLong, c);
910 else
911 return errorSyntax(USAGE_SHOWHDINFO, "error: %Rrs", c);
912 }
913 }
914
915 /* check for required options */
916 if (!FilenameOrUuid)
917 return errorSyntax(USAGE_SHOWHDINFO, "Disk name or UUID required");
918
919 ComPtr<IMedium> hardDisk;
920 bool unknown = false;
921 /* first guess is that it's a UUID */
922 rc = a->virtualBox->FindMedium(Bstr(FilenameOrUuid).raw(),
923 DeviceType_HardDisk,
924 hardDisk.asOutParam());
925 /* no? well, then it's an unknown image */
926 if (FAILED(rc))
927 {
928 rc = a->virtualBox->OpenMedium(Bstr(FilenameOrUuid).raw(),
929 DeviceType_HardDisk,
930 AccessMode_ReadWrite,
931 hardDisk.asOutParam());
932 if (rc == VBOX_E_FILE_ERROR)
933 {
934 char szFilenameAbs[RTPATH_MAX] = "";
935 int vrc = RTPathAbs(FilenameOrUuid, szFilenameAbs, sizeof(szFilenameAbs));
936 if (RT_FAILURE(vrc))
937 {
938 RTMsgError("Cannot convert filename \"%s\" to absolute path", FilenameOrUuid);
939 return 1;
940 }
941 CHECK_ERROR(a->virtualBox, OpenMedium(Bstr(szFilenameAbs).raw(),
942 DeviceType_HardDisk,
943 AccessMode_ReadWrite,
944 hardDisk.asOutParam()));
945 }
946 else if (SUCCEEDED(rc))
947 unknown = true;
948 else
949 {
950 com::GluePrintRCMessage(rc);
951 return 1;
952 }
953 }
954
955 do
956 {
957 Bstr uuid;
958 hardDisk->COMGETTER(Id)(uuid.asOutParam());
959 RTPrintf("UUID: %s\n", Utf8Str(uuid).c_str());
960
961 /* check for accessibility */
962 /// @todo NEWMEDIA check accessibility of all parents
963 /// @todo NEWMEDIA print the full state value
964 MediumState_T state;
965 CHECK_ERROR_BREAK(hardDisk, RefreshState(&state));
966 RTPrintf("Accessible: %s\n", state != MediumState_Inaccessible ? "yes" : "no");
967
968 if (state == MediumState_Inaccessible)
969 {
970 Bstr err;
971 CHECK_ERROR_BREAK(hardDisk, COMGETTER(LastAccessError)(err.asOutParam()));
972 RTPrintf("Access Error: %lS\n", err.raw());
973 }
974
975 Bstr description;
976 hardDisk->COMGETTER(Description)(description.asOutParam());
977 if (!description.isEmpty())
978 {
979 RTPrintf("Description: %lS\n", description.raw());
980 }
981
982 LONG64 logicalSize;
983 hardDisk->COMGETTER(LogicalSize)(&logicalSize);
984 RTPrintf("Logical size: %lld MBytes\n", logicalSize);
985 LONG64 actualSize;
986 hardDisk->COMGETTER(Size)(&actualSize);
987 RTPrintf("Current size on disk: %lld MBytes\n", actualSize >> 20);
988
989 ComPtr <IMedium> parent;
990 hardDisk->COMGETTER(Parent)(parent.asOutParam());
991
992 MediumType_T type;
993 hardDisk->COMGETTER(Type)(&type);
994 const char *typeStr = "unknown";
995 switch (type)
996 {
997 case MediumType_Normal:
998 if (!parent.isNull())
999 typeStr = "normal (differencing)";
1000 else
1001 typeStr = "normal (base)";
1002 break;
1003 case MediumType_Immutable:
1004 typeStr = "immutable";
1005 break;
1006 case MediumType_Writethrough:
1007 typeStr = "writethrough";
1008 break;
1009 case MediumType_Shareable:
1010 typeStr = "shareable";
1011 break;
1012 case MediumType_Readonly:
1013 typeStr = "readonly";
1014 break;
1015 case MediumType_MultiAttach:
1016 typeStr = "multiattach";
1017 break;
1018 }
1019 RTPrintf("Type: %s\n", typeStr);
1020
1021 Bstr format;
1022 hardDisk->COMGETTER(Format)(format.asOutParam());
1023 RTPrintf("Storage format: %lS\n", format.raw());
1024 MediumVariant_T variant;
1025 hardDisk->COMGETTER(Variant)(&variant);
1026 const char *variantStr = "unknown";
1027 switch (variant & ~(MediumVariant_Fixed | MediumVariant_Diff))
1028 {
1029 case MediumVariant_VmdkSplit2G:
1030 variantStr = "split2G";
1031 break;
1032 case MediumVariant_VmdkStreamOptimized:
1033 variantStr = "streamOptimized";
1034 break;
1035 case MediumVariant_VmdkESX:
1036 variantStr = "ESX";
1037 break;
1038 case MediumVariant_Standard:
1039 variantStr = "default";
1040 break;
1041 }
1042 const char *variantTypeStr = "dynamic";
1043 if (variant & MediumVariant_Fixed)
1044 variantTypeStr = "fixed";
1045 else if (variant & MediumVariant_Diff)
1046 variantTypeStr = "differencing";
1047 RTPrintf("Format variant: %s %s\n", variantTypeStr, variantStr);
1048
1049 /// @todo also dump config parameters (iSCSI)
1050
1051 if (!unknown)
1052 {
1053 com::SafeArray<BSTR> machineIds;
1054 hardDisk->COMGETTER(MachineIds)(ComSafeArrayAsOutParam(machineIds));
1055 for (size_t j = 0; j < machineIds.size(); ++ j)
1056 {
1057 ComPtr<IMachine> machine;
1058 CHECK_ERROR(a->virtualBox, FindMachine(machineIds[j], machine.asOutParam()));
1059 ASSERT(machine);
1060 Bstr name;
1061 machine->COMGETTER(Name)(name.asOutParam());
1062 machine->COMGETTER(Id)(uuid.asOutParam());
1063 RTPrintf("%s%lS (UUID: %lS)\n",
1064 j == 0 ? "In use by VMs: " : " ",
1065 name.raw(), machineIds[j]);
1066 }
1067 /// @todo NEWMEDIA check usage in snapshots too
1068 /// @todo NEWMEDIA also list children
1069 }
1070
1071 Bstr loc;
1072 hardDisk->COMGETTER(Location)(loc.asOutParam());
1073 RTPrintf("Location: %lS\n", loc.raw());
1074
1075 /* print out information specific for differencing hard disks */
1076 if (!parent.isNull())
1077 {
1078 BOOL autoReset = FALSE;
1079 hardDisk->COMGETTER(AutoReset)(&autoReset);
1080 RTPrintf("Auto-Reset: %s\n", autoReset ? "on" : "off");
1081 }
1082 }
1083 while (0);
1084
1085 if (unknown)
1086 {
1087 /* close the unknown hard disk to forget it again */
1088 hardDisk->Close();
1089 }
1090
1091 return SUCCEEDED(rc) ? 0 : 1;
1092}
1093
1094static const RTGETOPTDEF g_aCloseMediumOptions[] =
1095{
1096 { "disk", 'd', RTGETOPT_REQ_NOTHING },
1097 { "dvd", 'D', RTGETOPT_REQ_NOTHING },
1098 { "floppy", 'f', RTGETOPT_REQ_NOTHING },
1099 { "--delete", 'r', RTGETOPT_REQ_NOTHING },
1100};
1101
1102int handleCloseMedium(HandlerArg *a)
1103{
1104 HRESULT rc = S_OK;
1105 enum {
1106 CMD_NONE,
1107 CMD_DISK,
1108 CMD_DVD,
1109 CMD_FLOPPY
1110 } cmd = CMD_NONE;
1111 const char *FilenameOrUuid = NULL;
1112 bool fDelete = false;
1113
1114 int c;
1115 RTGETOPTUNION ValueUnion;
1116 RTGETOPTSTATE GetState;
1117 // start at 0 because main() has hacked both the argc and argv given to us
1118 RTGetOptInit(&GetState, a->argc, a->argv, g_aCloseMediumOptions, RT_ELEMENTS(g_aCloseMediumOptions),
1119 0, RTGETOPTINIT_FLAGS_NO_STD_OPTS);
1120 while ((c = RTGetOpt(&GetState, &ValueUnion)))
1121 {
1122 switch (c)
1123 {
1124 case 'd': // disk
1125 if (cmd != CMD_NONE)
1126 return errorSyntax(USAGE_CLOSEMEDIUM, "Only one command can be specified: '%s'", ValueUnion.psz);
1127 cmd = CMD_DISK;
1128 break;
1129
1130 case 'D': // DVD
1131 if (cmd != CMD_NONE)
1132 return errorSyntax(USAGE_CLOSEMEDIUM, "Only one command can be specified: '%s'", ValueUnion.psz);
1133 cmd = CMD_DVD;
1134 break;
1135
1136 case 'f': // floppy
1137 if (cmd != CMD_NONE)
1138 return errorSyntax(USAGE_CLOSEMEDIUM, "Only one command can be specified: '%s'", ValueUnion.psz);
1139 cmd = CMD_FLOPPY;
1140 break;
1141
1142 case 'r': // --delete
1143 fDelete = true;
1144 break;
1145
1146 case VINF_GETOPT_NOT_OPTION:
1147 if (!FilenameOrUuid)
1148 FilenameOrUuid = ValueUnion.psz;
1149 else
1150 return errorSyntax(USAGE_CLOSEMEDIUM, "Invalid parameter '%s'", ValueUnion.psz);
1151 break;
1152
1153 default:
1154 if (c > 0)
1155 {
1156 if (RT_C_IS_PRINT(c))
1157 return errorSyntax(USAGE_CLOSEMEDIUM, "Invalid option -%c", c);
1158 else
1159 return errorSyntax(USAGE_CLOSEMEDIUM, "Invalid option case %i", c);
1160 }
1161 else if (c == VERR_GETOPT_UNKNOWN_OPTION)
1162 return errorSyntax(USAGE_CLOSEMEDIUM, "unknown option: %s\n", ValueUnion.psz);
1163 else if (ValueUnion.pDef)
1164 return errorSyntax(USAGE_CLOSEMEDIUM, "%s: %Rrs", ValueUnion.pDef->pszLong, c);
1165 else
1166 return errorSyntax(USAGE_CLOSEMEDIUM, "error: %Rrs", c);
1167 }
1168 }
1169
1170 /* check for required options */
1171 if (cmd == CMD_NONE)
1172 return errorSyntax(USAGE_CLOSEMEDIUM, "Command variant disk/dvd/floppy required");
1173 if (!FilenameOrUuid)
1174 return errorSyntax(USAGE_CLOSEMEDIUM, "Disk name or UUID required");
1175
1176 ComPtr<IMedium> medium;
1177
1178 if (cmd == CMD_DISK)
1179 CHECK_ERROR(a->virtualBox, FindMedium(Bstr(FilenameOrUuid).raw(),
1180 DeviceType_HardDisk,
1181 medium.asOutParam()));
1182 else if (cmd == CMD_DVD)
1183 CHECK_ERROR(a->virtualBox, FindMedium(Bstr(FilenameOrUuid).raw(),
1184 DeviceType_DVD,
1185 medium.asOutParam()));
1186 else if (cmd == CMD_FLOPPY)
1187 CHECK_ERROR(a->virtualBox, FindMedium(Bstr(FilenameOrUuid).raw(),
1188 DeviceType_Floppy,
1189 medium.asOutParam()));
1190
1191 if (SUCCEEDED(rc) && medium)
1192 {
1193 if (fDelete)
1194 {
1195 ComPtr<IProgress> progress;
1196 CHECK_ERROR(medium, DeleteStorage(progress.asOutParam()));
1197 if (SUCCEEDED(rc))
1198 {
1199 rc = showProgress(progress);
1200 if (FAILED(rc))
1201 {
1202 com::ProgressErrorInfo info(progress);
1203 if (info.isBasicAvailable())
1204 RTMsgError("Failed to delete medium. Error message: %lS", info.getText().raw());
1205 else
1206 RTMsgError("Failed to delete medium. No error message available!");
1207 }
1208 }
1209 else
1210 RTMsgError("Failed to delete medium. Error code %Rrc", rc);
1211 }
1212 CHECK_ERROR(medium, Close());
1213 }
1214
1215 return SUCCEEDED(rc) ? 0 : 1;
1216}
1217#endif /* !VBOX_ONLY_DOCS */
注意: 瀏覽 TracBrowser 來幫助您使用儲存庫瀏覽器

© 2025 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette