18
18
use Symfony \Component \Console \Command \Command ;
19
19
use Symfony \Component \Console \Input \InputArgument ;
20
20
use Symfony \Component \Console \Input \InputInterface ;
21
- use Symfony \Component \Console \Style \SymfonyStyle ;
22
21
use Symfony \Component \Console \Output \OutputInterface ;
22
+ use Symfony \Component \Console \Style \SymfonyStyle ;
23
+ use Symfony \Component \DependencyInjection \ParameterBag \ParameterBagInterface ;
23
24
use Symfony \Component \Finder \Finder ;
24
25
25
-
26
26
#[AsCommand(
27
27
name: 'app:create-courses-from-structured-file ' ,
28
28
description: 'Create courses and learning paths from a folder containing files ' ,
@@ -32,20 +32,24 @@ class CreateCoursesFromStructuredFileCommand extends Command
32
32
public function __construct (
33
33
private readonly EntityManagerInterface $ em ,
34
34
private readonly CourseService $ courseService ,
35
- private readonly SettingsManager $ settingsManager
35
+ private readonly SettingsManager $ settingsManager ,
36
+ private readonly ParameterBagInterface $ parameterBag ,
36
37
) {
37
38
parent ::__construct ();
38
39
}
39
40
40
41
protected function configure (): void
41
42
{
42
- $ this
43
- ->addArgument ('folder ' , InputArgument::REQUIRED , 'Path to folder with course files ' );
43
+ $ this ->addArgument (
44
+ 'folder ' ,
45
+ InputArgument::REQUIRED ,
46
+ 'Absolute path to the folder that contains course files '
47
+ );
44
48
}
45
49
46
50
protected function execute (InputInterface $ input , OutputInterface $ output ): int
47
51
{
48
- $ io = new SymfonyStyle ($ input , $ output );
52
+ $ io = new SymfonyStyle ($ input , $ output );
49
53
$ adminUser = $ this ->getFirstAdmin ();
50
54
if (!$ adminUser ) {
51
55
$ io ->error ('No admin user found in the system. ' );
@@ -58,41 +62,52 @@ protected function execute(InputInterface $input, OutputInterface $output): int
58
62
return Command::FAILURE ;
59
63
}
60
64
65
+ // Retrieve Unix permissions from platform settings
66
+ $ dirPermOct = octdec (
67
+ $ this ->settingsManager ->getSetting ('document.permissions_for_new_directories ' ) ?? '0777 '
68
+ );
69
+ $ filePermOct = octdec (
70
+ $ this ->settingsManager ->getSetting ('document.permissions_for_new_files ' ) ?? '0666 '
71
+ );
72
+
73
+ // Absolute base to /var/upload/resource
74
+ $ uploadBase = $ this ->parameterBag ->get ('kernel.project_dir ' ) . '/var/upload/resource ' ;
75
+
61
76
$ finder = new Finder ();
62
77
$ finder ->files ()->in ($ folder );
63
78
64
79
foreach ($ finder as $ file ) {
65
- $ basename = $ file ->getBasename ();
80
+ $ basename = $ file ->getBasename ();
66
81
$ courseCode = pathinfo ($ basename , PATHINFO_FILENAME );
67
- $ filePath = $ file ->getRealPath ();
82
+ $ filePath = $ file ->getRealPath ();
68
83
69
- // Skip unsupported extensions
84
+ // 1. Skip unsupported file extensions
70
85
$ allowedExtensions = ['pdf ' , 'html ' , 'htm ' , 'mp4 ' ];
71
- if (!in_array (strtolower ($ file ->getExtension ()), $ allowedExtensions )) {
86
+ if (!in_array (strtolower ($ file ->getExtension ()), $ allowedExtensions, true )) {
72
87
$ io ->warning ("Skipping unsupported file: $ basename " );
73
88
continue ;
74
89
}
75
90
76
91
$ io ->section ("Creating course: $ courseCode " );
77
92
78
- // Step 1: Create the course
93
+ // 2. Create course
79
94
$ course = $ this ->courseService ->createCourse ([
80
- 'title ' => $ courseCode ,
81
- 'wanted_code ' => $ courseCode ,
95
+ 'title ' => $ courseCode ,
96
+ 'wanted_code ' => $ courseCode ,
82
97
'add_user_as_teacher ' => true ,
83
- 'course_language ' => $ this ->settingsManager ->getSetting ('language.platform_language ' ),
84
- 'visibility ' => Course::OPEN_PLATFORM ,
85
- 'subscribe ' => true ,
86
- 'unsubscribe ' => true ,
87
- 'disk_quota ' => $ this ->settingsManager ->getSetting ('document.default_document_quotum ' ),
88
- 'expiration_date ' => (new \DateTime ('+1 year ' ))->format ('Y-m-d H:i:s ' )
98
+ 'course_language ' => $ this ->settingsManager ->getSetting ('language.platform_language ' ),
99
+ 'visibility ' => Course::OPEN_PLATFORM ,
100
+ 'subscribe ' => true ,
101
+ 'unsubscribe ' => true ,
102
+ 'disk_quota ' => $ this ->settingsManager ->getSetting ('document.default_document_quotum ' ),
103
+ 'expiration_date ' => (new \DateTime ('+1 year ' ))->format ('Y-m-d H:i:s ' ),
89
104
]);
90
105
91
106
if (!$ course ) {
92
- throw new \RuntimeException (' Error: Course could not be created.' );
107
+ throw new \RuntimeException (" Course ' $ courseCode ' could not be created." );
93
108
}
94
109
95
- // Step 2: Create learning path (CLp)
110
+ // 3. Create learning path
96
111
$ lp = (new CLp ())
97
112
->setLpType (1 )
98
113
->setTitle ($ courseCode )
@@ -101,15 +116,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
101
116
->setExpiredOn (null )
102
117
->setCategory (null )
103
118
->setParent ($ course )
104
- ->addCourseLink ($ course );
105
- $ lp ->setCreator ($ adminUser );
119
+ ->addCourseLink ($ course )
120
+ ->setCreator ($ adminUser );
106
121
107
- $ lpRepo = $ this ->em ->getRepository (CLp::class);
108
- $ lpRepo ->createLp ($ lp );
122
+ $ this ->em ->getRepository (CLp::class)->createLp ($ lp );
109
123
110
- // Step 3: Create CDocument from uploaded file
111
- $ document = new CDocument ();
112
- $ document ->setFiletype ('file ' )
124
+ // 4. Create document
125
+ $ document = ( new CDocument ())
126
+ ->setFiletype ('file ' )
113
127
->setTitle ($ basename )
114
128
->setComment (null )
115
129
->setReadonly (false )
@@ -121,12 +135,26 @@ protected function execute(InputInterface $input, OutputInterface $output): int
121
135
$ this ->em ->flush ();
122
136
123
137
$ documentRepo = $ this ->em ->getRepository (CDocument::class);
124
- $ documentRepo ->addFileFromPath ($ document , $ basename , $ filePath );
138
+ $ resourceFile = $ documentRepo ->addFileFromPath ($ document , $ basename , $ filePath );
139
+
140
+ // 4.1 Apply permissions to the real file & its directory
141
+ if ($ resourceFile ) {
142
+ $ resourceNodeRepo = $ this ->em ->getRepository (\Chamilo \CoreBundle \Entity \ResourceNode::class);
143
+ $ relativePath = $ resourceNodeRepo ->getFilename ($ resourceFile ); // e.g. /2025/06/16/abc.pdf
144
+ $ fullPath = realpath ($ uploadBase . $ relativePath );
145
+
146
+ if ($ fullPath && is_file ($ fullPath )) {
147
+ @chmod ($ fullPath , $ filePermOct );
148
+ }
149
+ $ fullDir = dirname ($ fullPath ?: '' );
150
+ if ($ fullDir && is_dir ($ fullDir )) {
151
+ @chmod ($ fullDir , $ dirPermOct );
152
+ }
153
+ }
125
154
126
- // Step 4: Create LP item linked to the document
127
- // Ensure root item exists
155
+ // 5. Ensure learning path root item exists
128
156
$ lpItemRepo = $ this ->em ->getRepository (CLpItem::class);
129
- $ rootItem = $ lpItemRepo ->getRootItem ((int ) $ lp ->getIid ());
157
+ $ rootItem = $ lpItemRepo ->getRootItem ((int ) $ lp ->getIid ());
130
158
131
159
if (!$ rootItem ) {
132
160
$ rootItem = (new CLpItem ())
@@ -138,6 +166,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
138
166
$ this ->em ->flush ();
139
167
}
140
168
169
+ // 6. Create LP item linked to the document
141
170
$ lpItem = (new CLpItem ())
142
171
->setLp ($ lp )
143
172
->setTitle ($ basename )
@@ -155,12 +184,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
155
184
$ this ->em ->persist ($ lpItem );
156
185
$ this ->em ->flush ();
157
186
158
- $ io ->success ("Course ' $ courseCode' created with LP and document item ' $ basename' " );
187
+ $ io ->success ("Course ' $ courseCode' created with LP and document ' $ basename' " );
159
188
}
160
189
161
190
return Command::SUCCESS ;
162
191
}
163
192
193
+ /**
194
+ * Return the first user that has ROLE_ADMIN.
195
+ */
164
196
private function getFirstAdmin (): ?User
165
197
{
166
198
return $ this ->em ->getRepository (User::class)
0 commit comments