More CD fixes
[ntk/apt.git] / cmdline / apt-cdrom.cc
1 // -*- mode: cpp; mode: fold -*-
2 // Description /*{{{*/
3 // $Id: apt-cdrom.cc,v 1.34 2000/01/17 07:11:49 jgg Exp $
4 /* ######################################################################
5
6 APT CDROM - Tool for handling APT's CDROM database.
7
8 Currently the only option is 'add' which will take the current CD
9 in the drive and add it into the database.
10
11 ##################################################################### */
12 /*}}}*/
13 // Include Files /*{{{*/
14 #include <apt-pkg/cmndline.h>
15 #include <apt-pkg/error.h>
16 #include <apt-pkg/init.h>
17 #include <apt-pkg/fileutl.h>
18 #include <apt-pkg/progress.h>
19 #include <apt-pkg/cdromutl.h>
20 #include <apt-pkg/strutl.h>
21 #include <config.h>
22
23 #include "indexcopy.h"
24
25 #include <iostream>
26 #include <fstream>
27 #include <vector>
28 #include <algorithm>
29 #include <sys/stat.h>
30 #include <fcntl.h>
31 #include <dirent.h>
32 #include <unistd.h>
33 #include <stdio.h>
34 /*}}}*/
35
36 // FindPackages - Find the package files on the CDROM /*{{{*/
37 // ---------------------------------------------------------------------
38 /* We look over the cdrom for package files. This is a recursive
39 search that short circuits when it his a package file in the dir.
40 This speeds it up greatly as the majority of the size is in the
41 binary-* sub dirs. */
42 bool FindPackages(string CD,vector<string> &List,vector<string> &SList,
43 string &InfoDir,unsigned int Depth = 0)
44 {
45 static ino_t Inodes[9];
46 if (Depth >= 7)
47 return true;
48
49 if (CD[CD.length()-1] != '/')
50 CD += '/';
51
52 if (chdir(CD.c_str()) != 0)
53 return _error->Errno("chdir","Unable to change to %s",CD.c_str());
54
55 // Look for a .disk subdirectory
56 struct stat Buf;
57 if (stat(".disk",&Buf) == 0)
58 {
59 if (InfoDir.empty() == true)
60 InfoDir = CD + ".disk/";
61 }
62
63 /* Aha! We found some package files. We assume that everything under
64 this dir is controlled by those package files so we don't look down
65 anymore */
66 if (stat("Packages",&Buf) == 0 || stat("Packages.gz",&Buf) == 0)
67 {
68 List.push_back(CD);
69
70 // Continue down if thorough is given
71 if (_config->FindB("APT::CDROM::Thorough",false) == false)
72 return true;
73 }
74 if (stat("Sources.gz",&Buf) == 0 || stat("Sources",&Buf) == 0)
75 {
76 SList.push_back(CD);
77
78 // Continue down if thorough is given
79 if (_config->FindB("APT::CDROM::Thorough",false) == false)
80 return true;
81 }
82
83 DIR *D = opendir(".");
84 if (D == 0)
85 return _error->Errno("opendir","Unable to read %s",CD.c_str());
86
87 // Run over the directory
88 for (struct dirent *Dir = readdir(D); Dir != 0; Dir = readdir(D))
89 {
90 // Skip some files..
91 if (strcmp(Dir->d_name,".") == 0 ||
92 strcmp(Dir->d_name,"..") == 0 ||
93 //strcmp(Dir->d_name,"source") == 0 ||
94 strcmp(Dir->d_name,".disk") == 0 ||
95 strcmp(Dir->d_name,"experimental") == 0 ||
96 strcmp(Dir->d_name,"binary-all") == 0)
97 continue;
98
99 // See if the name is a sub directory
100 struct stat Buf;
101 if (stat(Dir->d_name,&Buf) != 0)
102 continue;
103
104 if (S_ISDIR(Buf.st_mode) == 0)
105 continue;
106
107 unsigned int I;
108 for (I = 0; I != Depth; I++)
109 if (Inodes[I] == Buf.st_ino)
110 break;
111 if (I != Depth)
112 continue;
113
114 // Store the inodes weve seen
115 Inodes[Depth] = Buf.st_ino;
116
117 // Descend
118 if (FindPackages(CD + Dir->d_name,List,SList,InfoDir,Depth+1) == false)
119 break;
120
121 if (chdir(CD.c_str()) != 0)
122 return _error->Errno("chdir","Unable to change to ",CD.c_str());
123 };
124
125 closedir(D);
126
127 return !_error->PendingError();
128 }
129 /*}}}*/
130 // DropBinaryArch - Dump dirs with a string like /binary-<foo>/ /*{{{*/
131 // ---------------------------------------------------------------------
132 /* Here we drop everything that is not this machines arch */
133 bool DropBinaryArch(vector<string> &List)
134 {
135 char S[300];
136 sprintf(S,"/binary-%s/",_config->Find("Apt::Architecture").c_str());
137
138 for (unsigned int I = 0; I < List.size(); I++)
139 {
140 const char *Str = List[I].c_str();
141
142 const char *Res;
143 if ((Res = strstr(Str,"/binary-")) == 0)
144 continue;
145
146 // Weird, remove it.
147 if (strlen(Res) < strlen(S))
148 {
149 List.erase(List.begin() + I);
150 I--;
151 continue;
152 }
153
154 // See if it is our arch
155 if (stringcmp(Res,Res + strlen(S),S) == 0)
156 continue;
157
158 // Erase it
159 List.erase(List.begin() + I);
160 I--;
161 }
162
163 return true;
164 }
165 /*}}}*/
166 // Score - We compute a 'score' for a path /*{{{*/
167 // ---------------------------------------------------------------------
168 /* Paths are scored based on how close they come to what I consider
169 normal. That is ones that have 'dist' 'stable' 'frozen' will score
170 higher than ones without. */
171 int Score(string Path)
172 {
173 int Res = 0;
174 if (Path.find("stable/") != string::npos)
175 Res += 2;
176 if (Path.find("/binary-") != string::npos)
177 Res += 2;
178 if (Path.find("frozen/") != string::npos)
179 Res += 2;
180 if (Path.find("/dists/") != string::npos)
181 Res += 4;
182 if (Path.find("/main/") != string::npos)
183 Res += 2;
184 if (Path.find("/contrib/") != string::npos)
185 Res += 2;
186 if (Path.find("/non-free/") != string::npos)
187 Res += 2;
188 if (Path.find("/non-US/") != string::npos)
189 Res += 2;
190 if (Path.find("/source/") != string::npos)
191 Res += 1;
192 if (Path.find("/debian/") != string::npos)
193 Res -= 1;
194 return Res;
195 }
196 /*}}}*/
197 // DropRepeats - Drop repeated files resulting from symlinks /*{{{*/
198 // ---------------------------------------------------------------------
199 /* Here we go and stat every file that we found and strip dup inodes. */
200 bool DropRepeats(vector<string> &List,const char *Name)
201 {
202 // Get a list of all the inodes
203 ino_t *Inodes = new ino_t[List.size()];
204 for (unsigned int I = 0; I != List.size(); I++)
205 {
206 struct stat Buf;
207 if (stat((List[I] + Name).c_str(),&Buf) != 0 &&
208 stat((List[I] + Name + ".gz").c_str(),&Buf) != 0)
209 _error->Errno("stat","Failed to stat %s%s",List[I].c_str(),
210 Name);
211 Inodes[I] = Buf.st_ino;
212 }
213
214 if (_error->PendingError() == true)
215 return false;
216
217 // Look for dups
218 for (unsigned int I = 0; I != List.size(); I++)
219 {
220 for (unsigned int J = I+1; J < List.size(); J++)
221 {
222 // No match
223 if (Inodes[J] != Inodes[I])
224 continue;
225
226 // We score the two paths.. and erase one
227 int ScoreA = Score(List[I]);
228 int ScoreB = Score(List[J]);
229 if (ScoreA < ScoreB)
230 {
231 List[I] = string();
232 break;
233 }
234
235 List[J] = string();
236 }
237 }
238
239 // Wipe erased entries
240 for (unsigned int I = 0; I < List.size();)
241 {
242 if (List[I].empty() == false)
243 I++;
244 else
245 List.erase(List.begin()+I);
246 }
247
248 return true;
249 }
250 /*}}}*/
251
252 // ReduceSourceList - Takes the path list and reduces it /*{{{*/
253 // ---------------------------------------------------------------------
254 /* This takes the list of source list expressed entires and collects
255 similar ones to form a single entry for each dist */
256 bool ReduceSourcelist(string CD,vector<string> &List)
257 {
258 sort(List.begin(),List.end());
259
260 // Collect similar entries
261 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
262 {
263 // Find a space..
264 string::size_type Space = (*I).find(' ');
265 if (Space == string::npos)
266 continue;
267 string::size_type SSpace = (*I).find(' ',Space + 1);
268 if (SSpace == string::npos)
269 continue;
270
271 string Word1 = string(*I,Space,SSpace-Space);
272 for (vector<string>::iterator J = List.begin(); J != I; J++)
273 {
274 // Find a space..
275 string::size_type Space2 = (*J).find(' ');
276 if (Space2 == string::npos)
277 continue;
278 string::size_type SSpace2 = (*J).find(' ',Space2 + 1);
279 if (SSpace2 == string::npos)
280 continue;
281
282 if (string(*J,Space2,SSpace2-Space2) != Word1)
283 continue;
284
285 *J += string(*I,SSpace);
286 *I = string();
287 }
288 }
289
290 // Wipe erased entries
291 for (unsigned int I = 0; I < List.size();)
292 {
293 if (List[I].empty() == false)
294 I++;
295 else
296 List.erase(List.begin()+I);
297 }
298 }
299 /*}}}*/
300 // WriteDatabase - Write the CDROM Database file /*{{{*/
301 // ---------------------------------------------------------------------
302 /* We rewrite the configuration class associated with the cdrom database. */
303 bool WriteDatabase(Configuration &Cnf)
304 {
305 string DFile = _config->FindFile("Dir::State::cdroms");
306 string NewFile = DFile + ".new";
307
308 unlink(NewFile.c_str());
309 ofstream Out(NewFile.c_str());
310 if (!Out)
311 return _error->Errno("ofstream::ofstream",
312 "Failed to open %s.new",DFile.c_str());
313
314 /* Write out all of the configuration directives by walking the
315 configuration tree */
316 const Configuration::Item *Top = Cnf.Tree(0);
317 for (; Top != 0;)
318 {
319 // Print the config entry
320 if (Top->Value.empty() == false)
321 Out << Top->FullTag() + " \"" << Top->Value << "\";" << endl;
322
323 if (Top->Child != 0)
324 {
325 Top = Top->Child;
326 continue;
327 }
328
329 while (Top != 0 && Top->Next == 0)
330 Top = Top->Parent;
331 if (Top != 0)
332 Top = Top->Next;
333 }
334
335 Out.close();
336
337 rename(DFile.c_str(),string(DFile + '~').c_str());
338 if (rename(NewFile.c_str(),DFile.c_str()) != 0)
339 return _error->Errno("rename","Failed to rename %s.new to %s",
340 DFile.c_str(),DFile.c_str());
341
342 return true;
343 }
344 /*}}}*/
345 // WriteSourceList - Write an updated sourcelist /*{{{*/
346 // ---------------------------------------------------------------------
347 /* This reads the old source list and copies it into the new one. It
348 appends the new CDROM entires just after the first block of comments.
349 This places them first in the file. It also removes any old entries
350 that were the same. */
351 bool WriteSourceList(string Name,vector<string> &List,bool Source)
352 {
353 if (List.size() == 0)
354 return true;
355
356 string File = _config->FindFile("Dir::Etc::sourcelist");
357
358 // Open the stream for reading
359 ifstream F(File.c_str(),ios::in | ios::nocreate);
360 if (!F != 0)
361 return _error->Errno("ifstream::ifstream","Opening %s",File.c_str());
362
363 string NewFile = File + ".new";
364 unlink(NewFile.c_str());
365 ofstream Out(NewFile.c_str());
366 if (!Out)
367 return _error->Errno("ofstream::ofstream",
368 "Failed to open %s.new",File.c_str());
369
370 // Create a short uri without the path
371 string ShortURI = "cdrom:[" + Name + "]/";
372 string ShortURI2 = "cdrom:" + Name + "/"; // For Compatibility
373
374 const char *Type;
375 if (Source == true)
376 Type = "deb-src";
377 else
378 Type = "deb";
379
380 char Buffer[300];
381 int CurLine = 0;
382 bool First = true;
383 while (F.eof() == false)
384 {
385 F.getline(Buffer,sizeof(Buffer));
386 CurLine++;
387 _strtabexpand(Buffer,sizeof(Buffer));
388 _strstrip(Buffer);
389
390 // Comment or blank
391 if (Buffer[0] == '#' || Buffer[0] == 0)
392 {
393 Out << Buffer << endl;
394 continue;
395 }
396
397 if (First == true)
398 {
399 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
400 {
401 string::size_type Space = (*I).find(' ');
402 if (Space == string::npos)
403 return _error->Error("Internal error");
404 Out << Type << " cdrom:[" << Name << "]/" << string(*I,0,Space) <<
405 " " << string(*I,Space+1) << endl;
406 }
407 }
408 First = false;
409
410 // Grok it
411 string cType;
412 string URI;
413 const char *C = Buffer;
414 if (ParseQuoteWord(C,cType) == false ||
415 ParseQuoteWord(C,URI) == false)
416 {
417 Out << Buffer << endl;
418 continue;
419 }
420
421 // Emit lines like this one
422 if (cType != Type || (string(URI,0,ShortURI.length()) != ShortURI &&
423 string(URI,0,ShortURI.length()) != ShortURI2))
424 {
425 Out << Buffer << endl;
426 continue;
427 }
428 }
429
430 // Just in case the file was empty
431 if (First == true)
432 {
433 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
434 {
435 string::size_type Space = (*I).find(' ');
436 if (Space == string::npos)
437 return _error->Error("Internal error");
438
439 Out << "deb cdrom:[" << Name << "]/" << string(*I,0,Space) <<
440 " " << string(*I,Space+1) << endl;
441 }
442 }
443
444 Out.close();
445
446 rename(File.c_str(),string(File + '~').c_str());
447 if (rename(NewFile.c_str(),File.c_str()) != 0)
448 return _error->Errno("rename","Failed to rename %s.new to %s",
449 File.c_str(),File.c_str());
450
451 return true;
452 }
453 /*}}}*/
454
455 // Prompt - Simple prompt /*{{{*/
456 // ---------------------------------------------------------------------
457 /* */
458 void Prompt(const char *Text)
459 {
460 char C;
461 cout << Text << ' ' << flush;
462 read(STDIN_FILENO,&C,1);
463 if (C != '\n')
464 cout << endl;
465 }
466 /*}}}*/
467 // PromptLine - Prompt for an input line /*{{{*/
468 // ---------------------------------------------------------------------
469 /* */
470 string PromptLine(const char *Text)
471 {
472 cout << Text << ':' << endl;
473
474 string Res;
475 getline(cin,Res);
476 return Res;
477 }
478 /*}}}*/
479
480 // DoAdd - Add a new CDROM /*{{{*/
481 // ---------------------------------------------------------------------
482 /* This does the main add bit.. We show some status and things. The
483 sequence is to mount/umount the CD, Ident it then scan it for package
484 files and reduce that list. Then we copy over the package files and
485 verify them. Then rewrite the database files */
486 bool DoAdd(CommandLine &)
487 {
488 // Startup
489 string CDROM = _config->FindDir("Acquire::cdrom::mount","/cdrom/");
490 if (CDROM[0] == '.')
491 CDROM= SafeGetCWD() + '/' + CDROM;
492
493 cout << "Using CD-ROM mount point " << CDROM << endl;
494
495 // Read the database
496 Configuration Database;
497 string DFile = _config->FindFile("Dir::State::cdroms");
498 if (FileExists(DFile) == true)
499 {
500 if (ReadConfigFile(Database,DFile) == false)
501 return _error->Error("Unable to read the cdrom database %s",
502 DFile.c_str());
503 }
504
505 // Unmount the CD and get the user to put in the one they want
506 if (_config->FindB("APT::CDROM::NoMount",false) == false)
507 {
508 cout << "Unmounting CD-ROM" << endl;
509 UnmountCdrom(CDROM);
510
511 // Mount the new CDROM
512 Prompt("Please insert a Disc in the drive and press enter");
513 cout << "Mounting CD-ROM" << endl;
514 if (MountCdrom(CDROM) == false)
515 return _error->Error("Failed to mount the cdrom.");
516 }
517
518 // Hash the CD to get an ID
519 cout << "Identifying.. " << flush;
520 string ID;
521 if (IdentCdrom(CDROM,ID) == false)
522 {
523 cout << endl;
524 return false;
525 }
526
527 cout << '[' << ID << ']' << endl;
528
529 cout << "Scanning Disc for index files.. " << flush;
530 // Get the CD structure
531 vector<string> List;
532 vector<string> sList;
533 string StartDir = SafeGetCWD();
534 string InfoDir;
535 if (FindPackages(CDROM,List,sList,InfoDir) == false)
536 {
537 cout << endl;
538 return false;
539 }
540
541 chdir(StartDir.c_str());
542
543 if (_config->FindB("Debug::aptcdrom",false) == true)
544 {
545 cout << "I found (binary):" << endl;
546 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
547 cout << *I << endl;
548 cout << "I found (source):" << endl;
549 for (vector<string>::iterator I = sList.begin(); I != sList.end(); I++)
550 cout << *I << endl;
551 }
552
553 // Fix up the list
554 DropBinaryArch(List);
555 DropRepeats(List,"Packages");
556 DropRepeats(sList,"Sources");
557 cout << "Found " << List.size() << " package indexes and " << sList.size() <<
558 " source indexes." << endl;
559
560 if (List.size() == 0)
561 return _error->Error("Unable to locate any package files, perhaps this is not a Debian Disc");
562
563 // Check if the CD is in the database
564 string Name;
565 if (Database.Exists("CD::" + ID) == false ||
566 _config->FindB("APT::CDROM::Rename",false) == true)
567 {
568 // Try to use the CDs label if at all possible
569 if (InfoDir.empty() == false &&
570 FileExists(InfoDir + "/info") == true)
571 {
572 ifstream F(string(InfoDir + "/info").c_str());
573 if (!F == 0)
574 getline(F,Name);
575
576 if (Name.empty() == false)
577 {
578 cout << "Found label '" << Name << "'" << endl;
579 Database.Set("CD::" + ID + "::Label",Name);
580 }
581 }
582
583 if (_config->FindB("APT::CDROM::Rename",false) == true ||
584 Name.empty() == true)
585 {
586 cout << "Please provide a name for this Disc, such as 'Debian 2.1r1 Disk 1'";
587 while (1)
588 {
589 Name = PromptLine("");
590 if (Name.empty() == false &&
591 Name.find('"') == string::npos &&
592 Name.find('[') == string::npos &&
593 Name.find(']') == string::npos)
594 break;
595 cout << "That is not a valid name, try again " << endl;
596 }
597 }
598 }
599 else
600 Name = Database.Find("CD::" + ID);
601
602 string::iterator J = Name.begin();
603 for (; J != Name.end(); J++)
604 if (*J == '"' || *J == ']' || *J == '[')
605 *J = '_';
606
607 Database.Set("CD::" + ID,Name);
608 cout << "This Disc is called:" << endl << " '" << Name << "'" << endl;
609
610 // Copy the package files to the state directory
611 PackageCopy Copy;
612 SourceCopy SrcCopy;
613 if (Copy.CopyPackages(CDROM,Name,List) == false ||
614 SrcCopy.CopyPackages(CDROM,Name,sList) == false)
615 return false;
616
617 ReduceSourcelist(CDROM,List);
618 ReduceSourcelist(CDROM,sList);
619
620 // Write the database and sourcelist
621 if (_config->FindB("APT::cdrom::NoAct",false) == false)
622 {
623 if (WriteDatabase(Database) == false)
624 return false;
625
626 cout << "Writing new source list" << endl;
627 if (WriteSourceList(Name,List,false) == false ||
628 WriteSourceList(Name,sList,true) == false)
629 return false;
630 }
631
632 // Print the sourcelist entries
633 cout << "Source List entries for this Disc are:" << endl;
634 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
635 {
636 string::size_type Space = (*I).find(' ');
637 if (Space == string::npos)
638 return _error->Error("Internal error");
639
640 cout << "deb cdrom:[" << Name << "]/" << string(*I,0,Space) <<
641 " " << string(*I,Space+1) << endl;
642 }
643
644 for (vector<string>::iterator I = sList.begin(); I != sList.end(); I++)
645 {
646 string::size_type Space = (*I).find(' ');
647 if (Space == string::npos)
648 return _error->Error("Internal error");
649
650 cout << "deb-src cdrom:[" << Name << "]/" << string(*I,0,Space) <<
651 " " << string(*I,Space+1) << endl;
652 }
653
654 cout << "Repeat this process for the rest of the CDs in your set." << endl;
655
656 // Unmount and finish
657 if (_config->FindB("APT::CDROM::NoMount",false) == false)
658 UnmountCdrom(CDROM);
659
660 return true;
661 }
662 /*}}}*/
663
664 // ShowHelp - Show the help screen /*{{{*/
665 // ---------------------------------------------------------------------
666 /* */
667 int ShowHelp()
668 {
669 cout << PACKAGE << ' ' << VERSION << " for " << ARCHITECTURE <<
670 " compiled on " << __DATE__ << " " << __TIME__ << endl;
671 if (_config->FindB("version") == true)
672 return 100;
673
674 cout << "Usage: apt-cdrom [options] command" << endl;
675 cout << endl;
676 cout << "apt-cdrom is a tool to add CDROM's to APT's source list. The " << endl;
677 cout << "CDROM mount point and device information is taken from apt.conf" << endl;
678 cout << "and /etc/fstab." << endl;
679 cout << endl;
680 cout << "Commands:" << endl;
681 cout << " add - Add a CDROM" << endl;
682 cout << endl;
683 cout << "Options:" << endl;
684 cout << " -h This help text" << endl;
685 cout << " -d CD-ROM mount point" << endl;
686 cout << " -r Rename a recognized CD-ROM" << endl;
687 cout << " -m No mounting" << endl;
688 cout << " -f Fast mode, don't check package files" << endl;
689 cout << " -a Thorough scan mode" << endl;
690 cout << " -c=? Read this configuration file" << endl;
691 cout << " -o=? Set an arbitary configuration option, eg -o dir::cache=/tmp" << endl;
692 cout << "See fstab(5)" << endl;
693 return 100;
694 }
695 /*}}}*/
696
697 int main(int argc,const char *argv[])
698 {
699 CommandLine::Args Args[] = {
700 {'h',"help","help",0},
701 {'v',"version","version",0},
702 {'d',"cdrom","Acquire::cdrom::mount",CommandLine::HasArg},
703 {'r',"rename","APT::CDROM::Rename",0},
704 {'m',"no-mount","APT::CDROM::NoMount",0},
705 {'f',"fast","APT::CDROM::Fast",0},
706 {'n',"just-print","APT::CDROM::NoAct",0},
707 {'n',"recon","APT::CDROM::NoAct",0},
708 {'n',"no-act","APT::CDROM::NoAct",0},
709 {'a',"thorough","APT::CDROM::Thorough",0},
710 {'c',"config-file",0,CommandLine::ConfigFile},
711 {'o',"option",0,CommandLine::ArbItem},
712 {0,0,0,0}};
713 CommandLine::Dispatch Cmds[] = {
714 {"add",&DoAdd},
715 {0,0}};
716
717 // Parse the command line and initialize the package library
718 CommandLine CmdL(Args,_config);
719 if (pkgInitialize(*_config) == false ||
720 CmdL.Parse(argc,argv) == false)
721 {
722 _error->DumpErrors();
723 return 100;
724 }
725
726 // See if the help should be shown
727 if (_config->FindB("help") == true ||
728 CmdL.FileSize() == 0)
729 return ShowHelp();
730
731 // Deal with stdout not being a tty
732 if (ttyname(STDOUT_FILENO) == 0 && _config->FindI("quiet",0) < 1)
733 _config->Set("quiet","1");
734
735 // Match the operation
736 CmdL.DispatchArg(Cmds);
737
738 // Print any errors or warnings found during parsing
739 if (_error->empty() == false)
740 {
741 bool Errors = _error->PendingError();
742 _error->DumpErrors();
743 return Errors == true?100:0;
744 }
745
746 return 0;
747 }